mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
refactor(settings): fold Sources into Integrationen as a sub-tab
Source priority decides which connection wins when two report the same metric, so it belongs with the integrations rather than as a standalone settings page. Sources renders as the third Integrations sub-tab (Connections / Channels / Sources); the standalone `sources` slug is removed from the shell nav, slug registry, and route dispatch, and `/settings/sources` 301-redirects to `/settings/integrations`.
This commit is contained in:
1 parent
d3d8731209
commit
7b80a38ea2
8 files changed
+92
-66
No files matched your search
@@ -121,6 +121,15 @@ const nextConfig: NextConfig = {
|
||||
destination: "/coach",
|
||||
permanent: true,
|
||||
},
|
||||
// v1.18.0 (S3) — Sources folded into Settings → Integrations as the
|
||||
// "Sources" sub-tab; the standalone `/settings/sources` page is gone.
|
||||
// 301-redirect so bookmarks, the PWA's cached navigation, and the old
|
||||
// cross-links keep resolving.
|
||||
{
|
||||
source: "/settings/sources",
|
||||
destination: "/settings/integrations",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
|
||||
@@ -17,12 +17,10 @@ import { MoodSection } from "@/components/settings/mood-section";
|
||||
import { NotificationsSection } from "@/components/settings/notifications-section";
|
||||
import { RemindersSection } from "@/components/settings/reminders-section";
|
||||
import { SectionPlaceholder } from "@/components/settings/section-placeholder";
|
||||
// v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are two
|
||||
// separate sections again. `ThresholdsSection` renders the target-range
|
||||
// editor; `SourcesSection` (standalone mode) renders the source-priority
|
||||
// ladders. Both are served by this dynamic route.
|
||||
// v1.18.0 (S3) — `sources` no longer has a standalone route; source priority
|
||||
// renders inside Settings → Integrations as the "Sources" sub-tab.
|
||||
// `/settings/sources` 301-redirects to `/settings/integrations` (next.config.ts).
|
||||
import { ThresholdsSection } from "@/components/settings/thresholds-section";
|
||||
import { SourcesSection } from "@/components/settings/sources-section";
|
||||
import {
|
||||
SETTINGS_SECTION_SLUGS,
|
||||
isSettingsSectionSlug,
|
||||
@@ -32,7 +30,7 @@ import { SettingsShell } from "@/components/settings/settings-shell";
|
||||
|
||||
/**
|
||||
* Dynamic settings section route. Each of the `SETTINGS_SECTION_SLUGS`
|
||||
* (16 today — 12 nav-visible sections plus the four routable-but-hidden
|
||||
* (15 today — 11 nav-visible sections plus the four routable-but-hidden
|
||||
* Layout child editors) is pre-rendered at build via
|
||||
* `generateStaticParams()` so the URLs are
|
||||
* statically known to Next.js, while the `dynamicParams = false` flag below
|
||||
@@ -64,7 +62,6 @@ const SECTION_COMPONENTS: Record<
|
||||
medications: MedicationsSection,
|
||||
mood: MoodSection,
|
||||
thresholds: ThresholdsSection,
|
||||
sources: () => <SourcesSection />,
|
||||
api: ApiSection,
|
||||
export: ExportSection,
|
||||
advanced: AdvancedSection,
|
||||
|
||||
@@ -45,9 +45,10 @@ describe("SETTINGS_SECTION_SLUGS", () => {
|
||||
// `api` and `advanced` so every "give me my data out" path lives in
|
||||
// one place.
|
||||
// v1.4.25 W5e added `sources` between `thresholds` and `ai`; v1.4.34
|
||||
// IW-D merged it into `thresholds`.
|
||||
// v1.8.7.1 — `sources` (Sources) is its own slug again, sitting
|
||||
// between `thresholds` (Targets) and `ai`.
|
||||
// IW-D merged it into `thresholds`; v1.8.7.1 split it back out.
|
||||
// v1.18.0 (S3) — `sources` is no longer a standalone slug: source
|
||||
// priority folded into Settings → Integrations as the "Sources"
|
||||
// sub-tab. `/settings/sources` 301-redirects to `/settings/integrations`.
|
||||
// v1.15.18 — `insights` sits between `dashboard` and `thresholds`: the
|
||||
// overview-arrange + pill-sort customise surface for `/insights`.
|
||||
// v1.16.10 — `medications` sits between `insights` and `thresholds`:
|
||||
@@ -77,7 +78,6 @@ describe("SETTINGS_SECTION_SLUGS", () => {
|
||||
"medications",
|
||||
"mood",
|
||||
"thresholds",
|
||||
"sources",
|
||||
"ai",
|
||||
"api",
|
||||
"export",
|
||||
@@ -211,13 +211,11 @@ describe("<SettingsShell>", () => {
|
||||
// About is back in the settings nav as the last entry (it also
|
||||
// stays linked from the sidebar user-card dropdown).
|
||||
expect(html).toContain('href="/settings/about"');
|
||||
// v1.8.7.1 — Targets and Sources are two separate nav entries again.
|
||||
// v1.18.0 (S3) — Targets keeps its own nav entry; Sources folded into
|
||||
// Integrations as a sub-tab, so it is no longer a standalone nav entry.
|
||||
expect(html).toContain('href="/settings/thresholds"');
|
||||
expect(html).toContain('href="/settings/sources"');
|
||||
expect(html).not.toContain('href="/settings/sources"');
|
||||
expect(html).toContain("Targets");
|
||||
// v1.12.0 — the source-priority entry adopts the canonical
|
||||
// "Source priority" / "Quellen-Priorität" label (handover §5).
|
||||
expect(html).toContain("Source priority");
|
||||
});
|
||||
|
||||
it("resolves every section title via the i18n provider — German", () => {
|
||||
@@ -235,10 +233,9 @@ describe("<SettingsShell>", () => {
|
||||
expect(html).toContain("Layout & Personalisierung");
|
||||
expect(html).not.toContain('href="/settings/medications"');
|
||||
expect(html).not.toContain('href="/settings/mood"');
|
||||
// v1.8.7.1 — Targets and Sources are two separate German nav
|
||||
// entries again: "Zielwerte" and "Quellen".
|
||||
// v1.18.0 (S3) — Targets keeps its German nav entry ("Zielwerte");
|
||||
// Sources folded into Integrations as a sub-tab.
|
||||
expect(html).toContain("Zielwerte");
|
||||
expect(html).toContain("Quellen");
|
||||
// v1.8.7.1 — the AI Insights section is named "KI-Auswertungen" in
|
||||
// German (the "KI" prefix makes the AI nature explicit).
|
||||
expect(html).toContain("KI-Auswertungen");
|
||||
@@ -248,8 +245,8 @@ describe("<SettingsShell>", () => {
|
||||
// "Über diese App" (About) is back as the last in-shell nav entry
|
||||
// (the sidebar user-card dropdown keeps its own link too).
|
||||
expect(html).toContain('href="/settings/about"');
|
||||
// v1.8.7.1 — both Targets and Sources nav entries are present.
|
||||
expect(html).toContain('href="/settings/sources"');
|
||||
// v1.18.0 (S3) — Sources is no longer a standalone nav entry.
|
||||
expect(html).not.toContain('href="/settings/sources"');
|
||||
});
|
||||
|
||||
it("does NOT surface the raw key when a translation resolves — guards against missing JSON entries", () => {
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
* per-channel status surface (moved here from Settings → Notifications,
|
||||
* where it never belonged: channels are delivery providers, not
|
||||
* reminder preferences). See `notification-channels-panel.tsx`.
|
||||
* 3. Sources — the per-metric source-priority + device-type ladders that
|
||||
* decide which connection wins when two report the same metric. Folded in
|
||||
* from the former standalone `/settings/sources` page. See
|
||||
* `sources-section.tsx` (sub-tab mode).
|
||||
*
|
||||
* `parseOAuthOutcome` / `oauthReasonKey` are re-exported from
|
||||
* `connections-panel.tsx` so existing unit-test imports keep resolving.
|
||||
@@ -21,6 +25,7 @@ import { useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
|
||||
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
|
||||
import { SourcesSection } from "@/components/settings/sources-section";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export {
|
||||
@@ -28,7 +33,7 @@ export {
|
||||
oauthReasonKey,
|
||||
} from "@/components/settings/integrations/connections-panel";
|
||||
|
||||
type IntegrationsTab = "connections" | "channels";
|
||||
type IntegrationsTab = "connections" | "channels" | "sources";
|
||||
|
||||
export function IntegrationsSection() {
|
||||
const { t } = useTranslations();
|
||||
@@ -59,6 +64,9 @@ export function IntegrationsSection() {
|
||||
<TabsTrigger value="channels">
|
||||
{t("settings.sections.integrations.tabs.channels")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sources">
|
||||
{t("settings.sections.integrations.tabs.sources")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="connections">
|
||||
@@ -67,6 +75,9 @@ export function IntegrationsSection() {
|
||||
<TabsContent value="channels">
|
||||
<NotificationChannelsPanel />
|
||||
</TabsContent>
|
||||
<TabsContent value="sources">
|
||||
<SourcesSection variant="subtab" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Download,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
Link2,
|
||||
Pill,
|
||||
@@ -47,7 +46,6 @@ const SLUG_ICON: Record<SettingsSectionSlug, LucideIcon> = {
|
||||
medications: Pill,
|
||||
mood: Smile,
|
||||
thresholds: SlidersHorizontal,
|
||||
sources: Layers,
|
||||
ai: Sparkles,
|
||||
api: KeyRound,
|
||||
export: Download,
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
* graph.
|
||||
*/
|
||||
|
||||
// v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are again two
|
||||
// separate settings pages, each its own left-nav entry and route. They were
|
||||
// merged into one "Targets & Sources" page in v1.4.34 IW-D, but the two
|
||||
// editors drive distinct mutation flows (`/api/user/thresholds` vs
|
||||
// `/api/auth/me/source-priority`) and reading them as one page made the
|
||||
// surface long; splitting them back keeps each concern self-contained.
|
||||
// Both slugs are served by the dynamic `[section]` route below.
|
||||
// v1.18.0 (S3) — `sources` is no longer a standalone slug. Source priority
|
||||
// folded into Settings → Integrations as the "Sources" sub-tab (deciding which
|
||||
// connection wins when two report the same metric is an Integrations concern).
|
||||
// `/settings/sources` 301-redirects to `/settings/integrations` (next.config.ts).
|
||||
// `thresholds` (Targets) keeps its own left-nav entry + route, served by the
|
||||
// dynamic `[section]` route below.
|
||||
// v1.16.10 — `medications` sits after `insights`: the customise surface
|
||||
// for the /medications list (view preference + manual order), following
|
||||
// the dashboard / insights pattern of a dedicated settings section
|
||||
@@ -46,7 +45,6 @@ export const SETTINGS_SECTION_SLUGS = [
|
||||
"medications",
|
||||
"mood",
|
||||
"thresholds",
|
||||
"sources",
|
||||
"ai",
|
||||
"api",
|
||||
"export",
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
Download,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
Link2,
|
||||
Settings2,
|
||||
@@ -74,10 +73,10 @@ interface SettingsSection {
|
||||
* settings navigation. It is now both a regular (last) shell entry and
|
||||
* a user-card dropdown item ("Über HealthLog" / "About HealthLog").
|
||||
*
|
||||
* v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are two
|
||||
* separate sidebar entries again (merged into a single "Targets &
|
||||
* Sources" page in v1.4.34 IW-D, split back here). Both are served by
|
||||
* the dynamic `[section]` route.
|
||||
* v1.18.0 (S3) — `sources` is no longer a sidebar entry: source priority
|
||||
* folded into Settings → Integrations as the "Sources" sub-tab.
|
||||
* `thresholds` (Targets) keeps its own entry, served by the dynamic
|
||||
* `[section]` route.
|
||||
*/
|
||||
export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
{ slug: "account", titleKey: "settings.sections.account.title", icon: User },
|
||||
@@ -122,11 +121,6 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
titleKey: "settings.sections.thresholds.title",
|
||||
icon: SlidersHorizontal,
|
||||
},
|
||||
{
|
||||
slug: "sources",
|
||||
titleKey: "settings.sections.sources.title",
|
||||
icon: Layers,
|
||||
},
|
||||
{ slug: "ai", titleKey: "settings.sections.ai.title", icon: Sparkles },
|
||||
{ slug: "api", titleKey: "settings.sections.api.title", icon: KeyRound },
|
||||
{
|
||||
|
||||
@@ -111,18 +111,31 @@ const DEVICE_TYPE_LABEL_KEYS: Record<DeviceType, string> = {
|
||||
unknown: "settings.sections.sources.deviceLabels.unknown",
|
||||
};
|
||||
|
||||
export interface SourcesSectionProps {
|
||||
/**
|
||||
* `"standalone"` (the default) renders the full page header + the
|
||||
* cross-link back to Integrations — the shape used while Sources had its
|
||||
* own `/settings/sources` route. `"subtab"` (v1.18.0 S3) suppresses both:
|
||||
* Sources now renders inside the Integrations → Sources sub-tab, so the
|
||||
* page header is the parent section's and a cross-link back to the page
|
||||
* that already contains it would be circular.
|
||||
*/
|
||||
variant?: "standalone" | "subtab";
|
||||
}
|
||||
|
||||
/**
|
||||
* `<SourcesSection>` — route-level wrapper for `/settings/sources`
|
||||
* (the "Sources" / "Quellen" page).
|
||||
* `<SourcesSection>` — the per-metric source-priority + device-type ladders.
|
||||
*
|
||||
* v1.8.7.1 — Sources is its own settings page again (it was merged into
|
||||
* the combined "Targets & Sources" page under `/settings/thresholds` in
|
||||
* v1.4.34 IW-D). The section renders its own page header (h1 + subtitle)
|
||||
* and owns the per-metric source-priority + device-type ladders.
|
||||
* v1.18.0 (S3) — Sources folded into Settings → Integrations as the "Sources"
|
||||
* sub-tab (`variant="subtab"`), since deciding which connection wins when two
|
||||
* report the same metric is an Integrations concern. The standalone
|
||||
* `/settings/sources` route is gone; `/settings/sources` 301-redirects to
|
||||
* `/settings/integrations` (see `next.config.ts`).
|
||||
*/
|
||||
export function SourcesSection() {
|
||||
export function SourcesSection({ variant = "standalone" }: SourcesSectionProps = {}) {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const isSubtab = variant === "subtab";
|
||||
|
||||
const { data: remote, isLoading } = useQuery({
|
||||
queryKey: queryKeys.sourcePriority(),
|
||||
@@ -285,26 +298,35 @@ export function SourcesSection() {
|
||||
aria-labelledby="settings-section-sources-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header className="space-y-1">
|
||||
<h1 id="settings-section-sources-title" className="sr-only">
|
||||
{t("settings.sections.sources.title")}
|
||||
</h1>
|
||||
{isSubtab ? (
|
||||
// Sub-tab mode: the Integrations section owns the page header, and a
|
||||
// cross-link back to the page that already contains this tab would be
|
||||
// circular. Surface only the one-line description.
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.sources.description")}
|
||||
</p>
|
||||
{/* Cross-link back to Settings → Integrations — the page that
|
||||
actually adds / removes the sources this ladder ranks. */}
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.sections.sources.integrationsHint")}{" "}
|
||||
<Link
|
||||
href="/settings/integrations"
|
||||
className="text-primary underline underline-offset-2"
|
||||
data-slot="sources-integrations-cross-link"
|
||||
>
|
||||
{t("settings.sections.sources.integrationsHintLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</header>
|
||||
) : (
|
||||
<header className="space-y-1">
|
||||
<h1 id="settings-section-sources-title" className="sr-only">
|
||||
{t("settings.sections.sources.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.sources.description")}
|
||||
</p>
|
||||
{/* Cross-link back to Settings → Integrations — the page that
|
||||
actually adds / removes the sources this ladder ranks. */}
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.sections.sources.integrationsHint")}{" "}
|
||||
<Link
|
||||
href="/settings/integrations"
|
||||
className="text-primary underline underline-offset-2"
|
||||
data-slot="sources-integrations-cross-link"
|
||||
>
|
||||
{t("settings.sections.sources.integrationsHintLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCardHeader
|
||||
|
||||
Reference in new issue
Block a user