mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(settings): unlist Preview features until the topic pages ship
The switch was stored and shown, but nothing reads the topics flag yet, so the page promised a change it could not make. The section, its nav entry, its loader and its copy are gone, so its palette entry goes with them; /settings/preview is no longer a route. The stored flag, /api/settings/preview-features and its backup round-trip stay, and the column's pending-consumer note says why.
This commit is contained in:
1 parent
704822c010
commit
0918451e02
21 files changed
+23
-327
No files matched your search
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* Copy that promises something not yet shipped says so.
|
||||
*
|
||||
* The Topics preview switch exists before its pages do, so its description is
|
||||
* in the future tense in every locale's source (English) and in German; and
|
||||
* the workout-route switch is off for new accounts, which is what its detail
|
||||
* The workout-route switch is off for new accounts, which is what its detail
|
||||
* says rather than "off by default": an account that already held tracks at
|
||||
* the upgrade started with it on (migration 0434).
|
||||
* the upgrade started with it on (migration 0434). (The Topics preview switch
|
||||
* this file also held is unlisted until its pages ship, and its copy with it.)
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -13,11 +12,6 @@ import en from "../../messages/en.json";
|
||||
import de from "../../messages/de.json";
|
||||
|
||||
describe("settings copy tense", () => {
|
||||
it("describes the Topics preview as something it will add", () => {
|
||||
expect(en.settings.preview.topics.description).toMatch(/^Will add /);
|
||||
expect(de.settings.preview.topics.description).toMatch(/sobald/);
|
||||
});
|
||||
|
||||
it("says workout routes are off for new accounts", () => {
|
||||
expect(en.settings.privacy.workoutRoutes.detail).toMatch(
|
||||
/^Off for new accounts\./,
|
||||
|
||||
@@ -146,10 +146,7 @@ describe("historical Settings addresses", () => {
|
||||
// one silently. Module pages are new in v1.44 and are allowed here.
|
||||
const listed = new Set(HISTORICAL_SETTINGS_PATHS);
|
||||
const unlisted = routable.filter(
|
||||
(path) =>
|
||||
!listed.has(path) &&
|
||||
!path.startsWith("/settings/modules/") &&
|
||||
path !== "/settings/preview",
|
||||
(path) => !listed.has(path) && !path.startsWith("/settings/modules/"),
|
||||
);
|
||||
expect(unlisted).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { PreviewSection } from "../preview-section";
|
||||
import { primeLocales } from "@/__tests__/helpers/prime-locales";
|
||||
|
||||
primeLocales("de");
|
||||
|
||||
function render(topics: boolean | undefined, locale: "en" | "de" = "en") {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, refetchOnMount: false } },
|
||||
});
|
||||
if (topics !== undefined) {
|
||||
client.setQueryData(queryKeys.previewFeatures(), { topics });
|
||||
}
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<QueryClientProvider client={client}>
|
||||
<PreviewSection />
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<PreviewSection>", () => {
|
||||
it("shows the Topics switch off by default, saved on change", () => {
|
||||
const html = render(false);
|
||||
expect(html).toContain('data-slot="settings-preview-topics-switch"');
|
||||
expect(html).toMatch(
|
||||
/data-slot="settings-preview-topics-switch"[^>]*aria-checked="false"|aria-checked="false"[^>]*data-slot="settings-preview-topics-switch"/,
|
||||
);
|
||||
expect(html).toContain("Topics (beta)");
|
||||
// A switch, no Save: one save model per card.
|
||||
expect(html).not.toContain('type="submit"');
|
||||
});
|
||||
|
||||
it("reads on once the person switched it on", () => {
|
||||
expect(render(true)).toMatch(
|
||||
/data-slot="settings-preview-topics-switch"[^>]*aria-checked="true"|aria-checked="true"[^>]*data-slot="settings-preview-topics-switch"/,
|
||||
);
|
||||
});
|
||||
|
||||
it("says the existing pages stay as they are and nothing raw leaks", () => {
|
||||
const html = render(false, "de");
|
||||
expect(html).toContain("Themen (Beta)");
|
||||
expect(html).not.toContain("settings.preview.");
|
||||
});
|
||||
});
|
||||
@@ -170,12 +170,19 @@ describe("SETTINGS_SECTION_SLUGS", () => {
|
||||
// v1.23 — "Data & Privacy" assembles the export / deletion / retention /
|
||||
// encryption / session / activity pieces into one pane, after `advanced`.
|
||||
"privacy",
|
||||
// v1.44 — per-user preview opt-ins, in the System group before About.
|
||||
"preview",
|
||||
"about",
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists no Preview features page until the topic pages ship", () => {
|
||||
// The stored flag and its API stay; the page and its nav entry return
|
||||
// with the first preview the switch actually turns on.
|
||||
expect(isSettingsSectionSlug("preview")).toBe(false);
|
||||
expect(SETTINGS_SECTIONS.some((s) => s.slug === ("preview" as never))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("`isSettingsSectionSlug` accepts known slugs and rejects others", () => {
|
||||
for (const slug of SETTINGS_SECTION_SLUGS) {
|
||||
expect(isSettingsSectionSlug(slug)).toBe(true);
|
||||
@@ -242,7 +249,6 @@ describe("<SettingsShell>", () => {
|
||||
gesundheitsakte: "data",
|
||||
privacy: "data",
|
||||
advanced: "data",
|
||||
preview: "system",
|
||||
about: "system",
|
||||
});
|
||||
// Every routable section has a nav entry, and nothing else does.
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Settings > System > Preview features.
|
||||
*
|
||||
* One switch per preview feature, each off until the person turns it on, each
|
||||
* saved the moment it changes (`PATCH /api/settings/preview-features`). A
|
||||
* preview adds something beside what is there and never replaces a page.
|
||||
*
|
||||
* The `topics` flag is stored and shown here; the topic overview pages that
|
||||
* read it arrive in a later release, and the card says so rather than
|
||||
* promising a change the switch does not make yet.
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { LayoutDashboard } from "lucide-react";
|
||||
|
||||
import { toastWrittenOutcome } from "@/components/outcome/outcome-toast";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { InlineAction } from "@/components/ui/inline-action";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { apiGet, apiPatch } from "@/lib/api/api-fetch";
|
||||
import { showErrorToast } from "@/lib/errors/error-toast";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
DEFAULT_PREVIEW_FEATURES,
|
||||
type PreviewFeatures,
|
||||
} from "@/lib/settings/preview-features";
|
||||
|
||||
const DISCUSSIONS_URL = "https://github.com/MBombeck/HealthLog/discussions";
|
||||
|
||||
export function PreviewSection() {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const key = queryKeys.previewFeatures();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => apiGet<PreviewFeatures>("/api/settings/preview-features"),
|
||||
});
|
||||
const features = query.data ?? DEFAULT_PREVIEW_FEATURES;
|
||||
|
||||
const toggle = useMutation({
|
||||
mutationKey: key,
|
||||
mutationFn: (topics: boolean) =>
|
||||
apiPatch<PreviewFeatures>("/api/settings/preview-features", { topics }),
|
||||
onMutate: async (topics) => {
|
||||
await queryClient.cancelQueries({ queryKey: key });
|
||||
const previous = queryClient.getQueryData<PreviewFeatures>(key);
|
||||
queryClient.setQueryData<PreviewFeatures>(key, {
|
||||
...(previous ?? DEFAULT_PREVIEW_FEATURES),
|
||||
topics,
|
||||
});
|
||||
return { previous };
|
||||
},
|
||||
onSuccess: (saved, topics) => {
|
||||
if (saved) queryClient.setQueryData(key, saved);
|
||||
toastWrittenOutcome(
|
||||
"success",
|
||||
topics
|
||||
? t("settings.preview.topics.savedOn")
|
||||
: t("settings.preview.topics.savedOff"),
|
||||
);
|
||||
},
|
||||
onError: (failure, _topics, context) => {
|
||||
queryClient.setQueryData(key, context?.previous);
|
||||
showErrorToast(t("settings.savingError"), { cause: failure });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsCard data-slot="settings-preview-topics">
|
||||
<SettingsCardHeader
|
||||
anchor="preview-topics"
|
||||
icon={LayoutDashboard}
|
||||
title={t("settings.preview.topics.title")}
|
||||
titleId="settings-preview-topics-title"
|
||||
description={t("settings.preview.topics.description")}
|
||||
status={
|
||||
<Switch
|
||||
data-slot="settings-preview-topics-switch"
|
||||
checked={features.topics}
|
||||
disabled={query.isLoading || toggle.isPending}
|
||||
onCheckedChange={(next) => toggle.mutate(next)}
|
||||
aria-labelledby="settings-preview-topics-title"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{query.isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={query.error}
|
||||
slot="settings-preview-error"
|
||||
message={t("settings.preview.loadError")}
|
||||
onRetry={() => void query.refetch()}
|
||||
/>
|
||||
) : null}
|
||||
<p className="text-sm">{t("settings.preview.topics.detail")}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<InlineAction asChild tone="muted" size="xs">
|
||||
<Link
|
||||
href={DISCUSSIONS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("settings.preview.feedbackLink")}
|
||||
</Link>
|
||||
</InlineAction>
|
||||
</p>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -114,9 +114,10 @@ export const SETTINGS_SECTION_SLUGS = [
|
||||
// pieces into one coherent privacy pane. Sits next to `advanced` (the
|
||||
// destructive-action home) and before `about`.
|
||||
"privacy",
|
||||
// v1.44 — "Preview features": per-user opt-in to things finished enough to
|
||||
// try and not yet the default. Sits in the System group before About.
|
||||
"preview",
|
||||
// v1.44 — "Preview features" (per-user opt-ins, Settings > System) is not
|
||||
// listed until the first preview it switches, the topic pages, ships: a
|
||||
// switch that changes nothing is a promise the page cannot keep. The stored
|
||||
// flag, `/api/settings/preview-features` and its backup round-trip stay.
|
||||
"about",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -124,10 +124,6 @@ const LOADERS: Record<
|
||||
import("@/components/settings/privacy-section").then(
|
||||
(m) => m.PrivacySection,
|
||||
),
|
||||
preview: () =>
|
||||
import("@/components/settings/preview-section").then(
|
||||
(m) => m.PreviewSection,
|
||||
),
|
||||
};
|
||||
|
||||
const SECTIONS: Record<SettingsSectionSlug, ComponentType<SectionProps>> = {
|
||||
@@ -162,7 +158,6 @@ const SECTIONS: Record<SettingsSectionSlug, ComponentType<SectionProps>> = {
|
||||
export: dynamic(LOADERS.export, { loading: () => <SectionLoading /> }),
|
||||
advanced: dynamic(LOADERS.advanced, { loading: () => <SectionLoading /> }),
|
||||
privacy: dynamic(LOADERS.privacy, { loading: () => <SectionLoading /> }),
|
||||
preview: dynamic(LOADERS.preview, { loading: () => <SectionLoading /> }),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
CloudSun,
|
||||
Download,
|
||||
FileHeart,
|
||||
FlaskConical,
|
||||
Gauge,
|
||||
Info,
|
||||
KeyRound,
|
||||
@@ -275,12 +274,6 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
group: "data",
|
||||
},
|
||||
// ── System ──
|
||||
{
|
||||
slug: "preview",
|
||||
titleKey: "settings.sections.preview.title",
|
||||
icon: FlaskConical,
|
||||
group: "system",
|
||||
},
|
||||
{
|
||||
slug: "about",
|
||||
titleKey: "settings.sections.about.title",
|
||||
|
||||
@@ -44,9 +44,6 @@ export const settingsKeys = {
|
||||
*/
|
||||
moduleListLayout: () => ["settings", "module-list-layout"] as const,
|
||||
|
||||
/** The per-user preview-feature opt-ins behind `/api/settings/preview-features`. */
|
||||
previewFeatures: () => ["settings", "preview-features"] as const,
|
||||
|
||||
/**
|
||||
* v1.35.0 — which pillars count toward the Health Score, behind
|
||||
* `GET/PATCH /api/auth/me/health-score-config`. Its own cell rather than
|
||||
|
||||
@@ -98,7 +98,6 @@ export const SETTINGS_DESTINATION_INVENTORY = {
|
||||
export: PERSONAL,
|
||||
advanced: PERSONAL,
|
||||
privacy: PERSONAL,
|
||||
preview: PERSONAL,
|
||||
about: PERSONAL,
|
||||
} as const satisfies Record<
|
||||
SettingsSectionSlug,
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
*
|
||||
* Flags:
|
||||
* - `topics`: topic overview pages above the single-metric Insights pages.
|
||||
* Stored and shown in Settings > System; no surface reads it yet.
|
||||
* Stored, served by `/api/settings/preview-features` and carried by the
|
||||
* backup. The Settings > System switch is not listed until the topic pages
|
||||
* ship, because nothing reads the flag yet.
|
||||
*
|
||||
* Unknown keys in a stored blob are dropped on read, so a flag retired later
|
||||
* disappears cleanly and a malformed blob reads as everything off.
|
||||
|
||||
Reference in new issue
Block a user