use display name function

This commit is contained in:
miloschwartz
2026-01-19 20:49:39 -08:00
parent e09cd6c16c
commit b299f3d6aa
14 changed files with 121 additions and 26 deletions

View File

@@ -80,7 +80,8 @@ async function queryApprovals(
user: { user: {
name: users.name, name: users.name,
userId: users.userId, userId: users.userId,
username: users.username username: users.username,
email: users.email
} }
}) })
.from(approvals) .from(approvals)

View File

@@ -1,5 +1,6 @@
import { internal } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { ListUsersResponse } from "@server/routers/user"; import { ListUsersResponse } from "@server/routers/user";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import UsersTable, { UserRow } from "../../../../../components/UsersTable"; import UsersTable, { UserRow } from "../../../../../components/UsersTable";
@@ -73,7 +74,11 @@ export default async function UsersPage(props: UsersPageProps) {
return { return {
id: user.id, id: user.id,
username: user.username, username: user.username,
displayUsername: user.email || user.name || user.username, displayUsername: getUserDisplayName({
email: user.email,
name: user.name,
username: user.username
}),
name: user.name, name: user.name,
email: user.email, email: user.email,
type: user.type, type: user.type,

View File

@@ -40,6 +40,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useResourceContext } from "@app/hooks/useResourceContext"; import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { orgQueries, resourceQueries } from "@app/lib/queries"; import { orgQueries, resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -154,7 +155,10 @@ export default function ResourceAuthenticationPage() {
const allUsers = useMemo(() => { const allUsers = useMemo(() => {
return orgUsers.map((user) => ({ return orgUsers.map((user) => ({
id: user.id.toString(), id: user.id.toString(),
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` text: `${getUserDisplayName({
email: user.email,
username: user.username
})}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
})); }));
}, [orgUsers]); }, [orgUsers]);
@@ -229,7 +233,10 @@ export default function ResourceAuthenticationPage() {
"users", "users",
resourceUsers.map((i) => ({ resourceUsers.map((i) => ({
id: i.userId.toString(), id: i.userId.toString(),
text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` text: `${getUserDisplayName({
email: i.email,
username: i.username
})}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}`
})) }))
); );

View File

@@ -2,6 +2,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { AdminGetUserResponse } from "@server/routers/user/adminGetUser"; import { AdminGetUserResponse } from "@server/routers/user/adminGetUser";
import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { cache } from "react"; import { cache } from "react";
@@ -44,7 +45,15 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
return ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
title={`${user?.email || user?.name || user?.username}`} title={
user
? getUserDisplayName({
email: user.email,
name: user.name,
username: user.username
})
: ""
}
description={t("userDescription2")} description={t("userDescription2")}
/> />
<HorizontalTabs items={navItems}>{children}</HorizontalTabs> <HorizontalTabs items={navItems}>{children}</HorizontalTabs>

View File

@@ -1,6 +1,7 @@
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import DeviceLoginForm from "@/components/DeviceLoginForm"; import DeviceLoginForm from "@/components/DeviceLoginForm";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { cache } from "react"; import { cache } from "react";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -24,7 +25,12 @@ export default async function DeviceLoginPage({ searchParams }: Props) {
); );
} }
const userName = user?.name || user?.username || ""; const userName = user
? getUserDisplayName({
name: user.name,
username: user.username
})
: "";
return ( return (
<DeviceLoginForm <DeviceLoginForm

View File

@@ -11,6 +11,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { import {
@@ -321,10 +322,13 @@ export default function UsersTable({ users }: Props) {
<div className="space-y-2"> <div className="space-y-2">
<p> <p>
{t("userQuestionRemove", { {t("userQuestionRemove", {
selectedUser: selectedUser: selected
selected?.email || ? getUserDisplayName({
selected?.name || email: selected.email,
selected?.username name: selected.name,
username: selected.username
})
: ""
})} })}
</p> </p>
@@ -337,9 +341,11 @@ export default function UsersTable({ users }: Props) {
} }
buttonText={t("userDeleteConfirm")} buttonText={t("userDeleteConfirm")}
onConfirm={async () => deleteUser(selected!.id)} onConfirm={async () => deleteUser(selected!.id)}
string={ string={getUserDisplayName({
selected.email || selected.name || selected.username email: selected.email,
} name: selected.name,
username: selected.username
})}
title={t("userDeleteServer")} title={t("userDeleteServer")}
/> />
)} )}

View File

@@ -2,6 +2,7 @@
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { import {
approvalFiltersSchema, approvalFiltersSchema,
@@ -185,7 +186,11 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
<LaptopMinimal className="size-4 text-muted-foreground flex-none relative top-2 sm:top-0" /> <LaptopMinimal className="size-4 text-muted-foreground flex-none relative top-2 sm:top-0" />
<span> <span>
<span className="text-primary"> <span className="text-primary">
{approval.user.username} {getUserDisplayName({
email: approval.user.email,
name: approval.user.name,
username: approval.user.username
})}
</span> </span>
&nbsp; &nbsp;
{approval.type === "user_device" && ( {approval.type === "user_device" && (

View File

@@ -46,6 +46,7 @@ import { Switch } from "@app/components/ui/switch";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { orgQueries } from "@app/lib/queries"; import { orgQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -270,7 +271,10 @@ export default function CreateInternalResourceDialog({
const allUsers = usersResponse.map((user) => ({ const allUsers = usersResponse.map((user) => ({
id: user.id.toString(), id: user.id.toString(),
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` text: `${getUserDisplayName({
email: user.email,
username: user.username
})}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
})); }));
const allClients = clientsResponse const allClients = clientsResponse

View File

@@ -36,6 +36,7 @@ import {
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Tag, TagInput } from "@app/components/tags/tag-input";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
@@ -304,7 +305,10 @@ export default function EditInternalResourceDialog({
const allUsers = (usersQuery.data ?? []).map((user) => ({ const allUsers = (usersQuery.data ?? []).map((user) => ({
id: user.id.toString(), id: user.id.toString(),
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` text: `${getUserDisplayName({
email: user.email,
username: user.username
})}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
})); }));
const machineClients = (clientsQuery.data ?? []) const machineClients = (clientsQuery.data ?? [])
@@ -330,7 +334,10 @@ export default function EditInternalResourceDialog({
const formUsers = (resourceUsersQuery.data ?? []).map((i) => ({ const formUsers = (resourceUsersQuery.data ?? []).map((i) => ({
id: i.userId.toString(), id: i.userId.toString(),
text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` text: `${getUserDisplayName({
email: i.email,
username: i.username
})}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}`
})); }));
return { return {

View File

@@ -14,6 +14,7 @@ import {
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { Laptop, LogOut, Moon, Sun, Smartphone } from "lucide-react"; import { Laptop, LogOut, Moon, Sun, Smartphone } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -49,9 +50,8 @@ export default function ProfileIcon() {
const t = useTranslations(); const t = useTranslations();
function getInitials() { function getInitials() {
return (user.email || user.name || user.username) const displayName = getUserDisplayName({ user });
.substring(0, 1) return displayName.substring(0, 1).toUpperCase();
.toUpperCase();
} }
function handleThemeChange(theme: "light" | "dark" | "system") { function handleThemeChange(theme: "light" | "dark" | "system") {
@@ -109,7 +109,7 @@ export default function ProfileIcon() {
{t("signingAs")} {t("signingAs")}
</p> </p>
<p className="text-xs leading-none text-muted-foreground"> <p className="text-xs leading-none text-muted-foreground">
{user.email || user.name || user.username} {getUserDisplayName({ user })}
</p> </p>
</div> </div>
{user.serverAdmin ? ( {user.serverAdmin ? (

View File

@@ -12,6 +12,7 @@ import {
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { import {
ArrowRight, ArrowRight,
ArrowUpDown, ArrowUpDown,
@@ -344,7 +345,10 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
href={`/${r.orgId}/settings/access/users/${r.userId}`} href={`/${r.orgId}/settings/access/users/${r.userId}`}
> >
<Button variant="outline"> <Button variant="outline">
{r.userEmail || r.username || r.userId} {getUserDisplayName({
email: r.userEmail,
username: r.username
}) || r.userId}
<ArrowUpRight className="ml-2 h-4 w-4" /> <ArrowUpRight className="ml-2 h-4 w-4" />
</Button> </Button>
</Link> </Link>

View File

@@ -19,6 +19,7 @@ import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { formatAxiosError } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -271,10 +272,13 @@ export default function UsersTable({ users: u }: UsersTableProps) {
buttonText={t("userRemoveOrgConfirm")} buttonText={t("userRemoveOrgConfirm")}
onConfirm={removeUser} onConfirm={removeUser}
string={ string={
selectedUser?.email || selectedUser
selectedUser?.name || ? getUserDisplayName({
selectedUser?.username || email: selectedUser.email,
"" name: selectedUser.name,
username: selectedUser.username
})
: ""
} }
title={t("userRemoveOrg")} title={t("userRemoveOrg")}
/> />

View File

@@ -0,0 +1,36 @@
import { GetUserResponse } from "@server/routers/user";
type UserDisplayNameInput =
| {
user: GetUserResponse;
}
| {
email?: string | null;
name?: string | null;
username?: string | null;
};
/**
* Gets the display name for a user.
* Priority: email > name > username
*
* @param input - Either a user object or individual email, name, username properties
* @returns The display name string
*/
export function getUserDisplayName(input: UserDisplayNameInput): string {
let email: string | null | undefined;
let name: string | null | undefined;
let username: string | null | undefined;
if ("user" in input) {
email = input.user.email;
name = input.user.name;
username = input.user.username;
} else {
email = input.email;
name = input.name;
username = input.username;
}
return email || name || username || "";
}

View File

@@ -340,6 +340,7 @@ export type ApprovalItem = {
name: string | null; name: string | null;
userId: string; userId: string;
username: string; username: string;
email: string | null;
}; };
}; };