From 1c4bcce6c78705ffadcdf20ede68020e695d44a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Sun, 1 Nov 2026 00:56:24 +0100
Subject: [PATCH] fix(ui): name the account by its display name
The dashboard greeting, the Insights greeting, the top bar and the
sidebar read the username, so a display name set on the profile card
reached the reports but none of the surfaces that address the account.
They now read one helper that returns the display name when set and
falls back to the username otherwise.
Refs #1309
---
src/app/insights/page-client.tsx | 6 +++---
src/components/dashboard/dashboard-header.tsx | 3 ++-
.../layout/__tests__/sidebar-nav.test.tsx | 20 ++++++++++++++++++
src/components/layout/sidebar-nav.tsx | 20 +++++++++---------
src/components/layout/top-bar.tsx | 7 ++++---
src/lib/__tests__/account-name.test.ts | 21 +++++++++++++++++++
src/lib/account-name.ts | 13 ++++++++++++
7 files changed, 73 insertions(+), 17 deletions(-)
create mode 100644 src/lib/__tests__/account-name.test.ts
create mode 100644 src/lib/account-name.ts
diff --git a/src/app/insights/page-client.tsx b/src/app/insights/page-client.tsx
index de0233ea3..fb3c162be 100644
--- a/src/app/insights/page-client.tsx
+++ b/src/app/insights/page-client.tsx
@@ -7,6 +7,7 @@ import Link from "next/link";
import { SlidersHorizontal, TrendingUp } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
+import { accountName } from "@/lib/account-name";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
// Bare number — its own React- and label-map-free module, so importing it here
@@ -419,10 +420,9 @@ export default function InsightsPageClient() {
// boundary hydrates, and a greeting that personalises during the
// hydration render disagrees with the name-less SSR text — React
// #418. See `useMounted`.
+ const shownName = user ? accountName(user).trim() : "";
const heroGreetingName =
- mounted && user?.username?.trim() && user.username.trim().length > 0
- ? user.username.split(/\s+/)[0]
- : null;
+ mounted && shownName.length > 0 ? shownName.split(/\s+/)[0] : null;
// The advisor read only fires while the briefing is available, so with it
// unavailable these stay null: the hero keeps its fallback subtitle and the
// trends row its built-in captions.
diff --git a/src/components/dashboard/dashboard-header.tsx b/src/components/dashboard/dashboard-header.tsx
index 1a7308bff..c4e70a63b 100644
--- a/src/components/dashboard/dashboard-header.tsx
+++ b/src/components/dashboard/dashboard-header.tsx
@@ -32,6 +32,7 @@ import {
import { PageHeader } from "@/components/ui/page-header";
import { useTranslations } from "@/lib/i18n/context";
import { useAuth } from "@/hooks/use-auth";
+import { accountName } from "@/lib/account-name";
import { useMounted } from "@/hooks/use-mounted";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { hourInTz } from "@/lib/tz/format";
@@ -143,7 +144,7 @@ export function DashboardHeader({
const greeting = greetingLine({
greeting: timeGreeting,
known: mounted && user != null,
- name: user?.username?.trim() ? user.username : null,
+ name: user ? accountName(user).trim() || null : null,
createdAt: user?.createdAt,
});
const welcomeText = t(greeting.key, greeting.values);
diff --git a/src/components/layout/__tests__/sidebar-nav.test.tsx b/src/components/layout/__tests__/sidebar-nav.test.tsx
index c6d519608..a79eda4d1 100644
--- a/src/components/layout/__tests__/sidebar-nav.test.tsx
+++ b/src/components/layout/__tests__/sidebar-nav.test.tsx
@@ -18,6 +18,7 @@ const mockUserRef = {
value: {
id: "u1",
username: "testuser",
+ displayName: null as string | null,
email: "user@example.com",
role: "USER" as "USER" | "ADMIN",
avatarUrl: null,
@@ -119,6 +120,25 @@ const GATED_HREFS = [
"/achievements",
] as const;
+describe(" account identity", () => {
+ afterEach(() => {
+ mockUserRef.value = { ...mockUserRef.value, displayName: null };
+ });
+
+ it("names the account by its display name when one is set", () => {
+ mockUserRef.value = { ...mockUserRef.value, displayName: "Jane Doe" };
+ const html = render();
+ expect(html).toContain(">Jane Doe
");
+ expect(html).toContain(">JD");
+ expect(html).not.toContain(">testuser");
+ });
+
+ it("falls back to the username without a display name", () => {
+ const html = render();
+ expect(html).toContain(">testuser");
+ });
+});
+
describe(" first-paint fail-closed module gate (v1.25.12)", () => {
it("renders core destinations on first paint regardless of the module map", () => {
const maps: Record[] = [
diff --git a/src/components/layout/sidebar-nav.tsx b/src/components/layout/sidebar-nav.tsx
index ff2c62444..28062cbb8 100644
--- a/src/components/layout/sidebar-nav.tsx
+++ b/src/components/layout/sidebar-nav.tsx
@@ -40,6 +40,7 @@ import {
import { cn } from "@/lib/utils";
import { Logo } from "@/components/ui/logo";
import { useAuth, useLogout } from "@/hooks/use-auth";
+import { accountName } from "@/lib/account-name";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { useNavModules } from "@/hooks/use-nav-modules";
import { useMounted } from "@/hooks/use-mounted";
@@ -153,6 +154,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
const sharedRecord = user?.accountAccess?.active != null;
if (!user) return null;
+ const name = accountName(user);
const themeIcon =
theme === "system" ? (
@@ -166,13 +168,13 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
const identityBlock = (
<>
- {avatarUrl && }
+ {avatarUrl && }
- {getInitials(user.username)}
+ {getInitials(name)}
-
{user.username}
+
{name}
{user.email && (
{user.email}
)}
@@ -282,11 +284,9 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
{t("nav.userMenu")}
- {avatarUrl && (
-
- )}
+ {avatarUrl && }
- {getInitials(user.username)}
+ {getInitials(name)}
@@ -311,13 +311,13 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
label={t("nav.accountSettings")}
>
- {avatarUrl && }
+ {avatarUrl && }
- {getInitials(user.username)}
+ {getInitials(name)}
-
{user.username}
+
{name}
{user.email && (
{user.email}
diff --git a/src/components/layout/top-bar.tsx b/src/components/layout/top-bar.tsx
index 23988f100..4d499d1a2 100644
--- a/src/components/layout/top-bar.tsx
+++ b/src/components/layout/top-bar.tsx
@@ -26,6 +26,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Skeleton } from "@/components/ui/skeleton";
import Link from "next/link";
import { useAuth, useLogout } from "@/hooks/use-auth";
+import { accountName } from "@/lib/account-name";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useTheme } from "@/components/providers";
import { useTranslations } from "@/lib/i18n/context";
@@ -143,13 +144,13 @@ export function TopBar() {
{t("nav.userMenu")}
{avatarUrl && (
-
+
)}
- {getInitials(user.username)}
+ {getInitials(accountName(user))}
- {user.username}
+ {accountName(user)}
diff --git a/src/lib/__tests__/account-name.test.ts b/src/lib/__tests__/account-name.test.ts
new file mode 100644
index 000000000..3d080ebb1
--- /dev/null
+++ b/src/lib/__tests__/account-name.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from "vitest";
+
+import { accountName } from "../account-name";
+
+describe("accountName", () => {
+ it("names the account by its display name when one is set", () => {
+ expect(accountName({ username: "jdoe", displayName: "Jane" })).toBe("Jane");
+ });
+
+ it("trims the display name it shows", () => {
+ expect(accountName({ username: "jdoe", displayName: " Jane " })).toBe(
+ "Jane",
+ );
+ });
+
+ it("falls back to the username when the display name is empty or absent", () => {
+ expect(accountName({ username: "jdoe", displayName: " " })).toBe("jdoe");
+ expect(accountName({ username: "jdoe", displayName: null })).toBe("jdoe");
+ expect(accountName({ username: "jdoe" })).toBe("jdoe");
+ });
+});
diff --git a/src/lib/account-name.ts b/src/lib/account-name.ts
new file mode 100644
index 000000000..2340acaa6
--- /dev/null
+++ b/src/lib/account-name.ts
@@ -0,0 +1,13 @@
+/**
+ * The name the app shows and addresses an account by: the display name when
+ * one is set, the username otherwise. Greetings, the top bar and the sidebar
+ * all read it here, so the display name edited on the profile card reaches
+ * every surface that names the account.
+ */
+export function accountName(user: {
+ username: string;
+ displayName?: string | null;
+}): string {
+ const displayName = user.displayName?.trim();
+ return displayName ? displayName : user.username;
+}