diff --git a/src/frontend/src/components/common/pageLayout/index.tsx b/src/frontend/src/components/common/pageLayout/index.tsx
index a077eadb31..6f5618b02f 100644
--- a/src/frontend/src/components/common/pageLayout/index.tsx
+++ b/src/frontend/src/components/common/pageLayout/index.tsx
@@ -1,4 +1,5 @@
import type { To } from "react-router-dom";
+import { useTranslation } from "react-i18next";
import { CustomBanner } from "@/customization/components/custom-banner";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import { Button } from "../../ui/button";
@@ -21,11 +22,16 @@ export default function PageLayout({
backTo?: To;
}) {
const navigate = useCustomNavigate();
+ const { t } = useTranslation();
return (
-
+
@@ -37,6 +43,7 @@ export default function PageLayout({
navigate(backTo);
}}
data-testid="back_page_button"
+ aria-label={t("navigation.goBack")}
>
)}
diff --git a/src/frontend/src/components/core/sidebarComponent/index.tsx b/src/frontend/src/components/core/sidebarComponent/index.tsx
index 1ba31000a7..fb3c9ecc40 100644
--- a/src/frontend/src/components/core/sidebarComponent/index.tsx
+++ b/src/frontend/src/components/core/sidebarComponent/index.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { CustomLink } from "@/customization/components/custom-link";
import { useIsMobile } from "@/hooks/use-mobile";
@@ -21,13 +22,19 @@ type SideBarButtonsComponentProps = {
};
const SideBarButtonsComponent = ({ items }: SideBarButtonsComponentProps) => {
+ const { t } = useTranslation();
const location = useLocation();
const pathname = location.pathname;
const isMobile = useIsMobile();
return (
-
+
diff --git a/src/frontend/src/locales/de.json b/src/frontend/src/locales/de.json
index e6d891e571..8fe393f381 100644
--- a/src/frontend/src/locales/de.json
+++ b/src/frontend/src/locales/de.json
@@ -1656,6 +1656,7 @@
"settings.mcpClient.title": "Langflow MCP-Client",
"settings.nav.apiKeys": "Langflow-API-Schlüssel",
"settings.nav.dbProviders": "Datenbankanbieter",
+ "settings.nav.label": "Einstellungsnavigation",
"settings.nav.general": "Allgemein",
"settings.nav.globalVariables": "Globale Variablen",
"settings.nav.mcpClient": "Langflow MCP-Client",
@@ -1672,6 +1673,8 @@
"settings.passwordTitle": "Speicherposition",
"settings.profilePictureCategory.People": "Personen",
"settings.profilePictureCategory.Space": "Bereich",
+ "settings.avatarAlt": "{{folder}} Avatar {{index}}",
+ "navigation.goBack": "Zurück",
"settings.profilePictureDescription": "Wähle das Bild aus, das als dein Profilbild angezeigt werden soll.",
"settings.profilePictureTitle": "Profilbild",
"settings.recordingKeyboard": "Aufnahme Ihres Keyboards",
diff --git a/src/frontend/src/locales/en.json b/src/frontend/src/locales/en.json
index 7d45f03447..c4083e3d61 100644
--- a/src/frontend/src/locales/en.json
+++ b/src/frontend/src/locales/en.json
@@ -288,6 +288,8 @@
"settings.profilePictureDescription": "Choose the image that appears as your profile picture.",
"settings.profilePictureCategory.People": "People",
"settings.profilePictureCategory.Space": "Space",
+ "settings.avatarAlt": "{{folder}} avatar {{index}}",
+ "navigation.goBack": "Go back",
"settings.apiKeysTitle": "Langflow API Keys",
"settings.apiKeys.columnName": "Name",
"settings.apiKeys.columnKey": "Key",
@@ -501,6 +503,7 @@
"sidebar.emptyMessage": "Start creating a project or flow",
"sidebar.knowledge": "Knowledge",
"sidebar.myFiles": "My Files",
+ "settings.nav.label": "Settings navigation",
"settings.nav.general": "General",
"settings.nav.mcpServers": "MCP Servers",
"settings.nav.mcpClient": "Langflow MCP Client",
diff --git a/src/frontend/src/locales/es.json b/src/frontend/src/locales/es.json
index 888c22b3bd..fdf7ed9d2d 100644
--- a/src/frontend/src/locales/es.json
+++ b/src/frontend/src/locales/es.json
@@ -1656,6 +1656,7 @@
"settings.mcpClient.title": "Cliente Langflow MCP",
"settings.nav.apiKeys": "Claves API de Langflow",
"settings.nav.dbProviders": "Proveedores de bases de datos",
+ "settings.nav.label": "Navegación de configuración",
"settings.nav.general": "Tema general",
"settings.nav.globalVariables": "Variables globales",
"settings.nav.mcpClient": "Cliente Langflow MCP",
@@ -1672,6 +1673,8 @@
"settings.passwordTitle": "Contraseña",
"settings.profilePictureCategory.People": "Personas",
"settings.profilePictureCategory.Space": "Espacio",
+ "settings.avatarAlt": "avatar {{folder}} {{index}}",
+ "navigation.goBack": "Volver",
"settings.profilePictureDescription": "Elige la imagen que quieras que aparezca como foto de perfil.",
"settings.profilePictureTitle": "Foto de perfil",
"settings.recordingKeyboard": "Grabar el teclado",
diff --git a/src/frontend/src/locales/fr.json b/src/frontend/src/locales/fr.json
index 4d75784435..60e77ff46e 100644
--- a/src/frontend/src/locales/fr.json
+++ b/src/frontend/src/locales/fr.json
@@ -1656,6 +1656,7 @@
"settings.mcpClient.title": "Client Langflow MCP",
"settings.nav.apiKeys": "Clés API Langflow",
"settings.nav.dbProviders": "Fournisseurs de bases de données",
+ "settings.nav.label": "Navigation des paramètres",
"settings.nav.general": "Général",
"settings.nav.globalVariables": "Variables globales",
"settings.nav.mcpClient": "Client Langflow MCP",
@@ -1672,6 +1673,8 @@
"settings.passwordTitle": "Mot de passe",
"settings.profilePictureCategory.People": "Personnes",
"settings.profilePictureCategory.Space": "Espace",
+ "settings.avatarAlt": "avatar {{folder}} {{index}}",
+ "navigation.goBack": "Retour",
"settings.profilePictureDescription": "Choisissez l'image qui apparaîtra comme photo de profil.",
"settings.profilePictureTitle": "Photo de profil",
"settings.recordingKeyboard": "Enregistrer votre clavier",
diff --git a/src/frontend/src/locales/ja.json b/src/frontend/src/locales/ja.json
index 35b358ed58..7857e71d74 100644
--- a/src/frontend/src/locales/ja.json
+++ b/src/frontend/src/locales/ja.json
@@ -1656,6 +1656,7 @@
"settings.mcpClient.title": "Langflow MCP クライアント",
"settings.nav.apiKeys": "Langflow API キー",
"settings.nav.dbProviders": "DBプロバイダー",
+ "settings.nav.label": "設定ナビゲーション",
"settings.nav.general": "一般",
"settings.nav.globalVariables": "グローバル変数",
"settings.nav.mcpClient": "Langflow MCP クライアント",
@@ -1672,6 +1673,8 @@
"settings.passwordTitle": "パスワード",
"settings.profilePictureCategory.People": "担当者",
"settings.profilePictureCategory.Space": "スペース",
+ "settings.avatarAlt": "{{folder}}アバター{{index}}",
+ "navigation.goBack": "戻る",
"settings.profilePictureDescription": "プロフィール写真として表示される画像を選んでください。",
"settings.profilePictureTitle": "プロフィール写真",
"settings.recordingKeyboard": "キーボードの録音",
diff --git a/src/frontend/src/locales/pt.json b/src/frontend/src/locales/pt.json
index 24a0ffa862..a0af433863 100644
--- a/src/frontend/src/locales/pt.json
+++ b/src/frontend/src/locales/pt.json
@@ -1656,6 +1656,7 @@
"settings.mcpClient.title": "Cliente Langflow MCP",
"settings.nav.apiKeys": "Chaves de API do Langflow",
"settings.nav.dbProviders": "Provedores de banco de dados",
+ "settings.nav.label": "Navegação de configurações",
"settings.nav.general": "Geral",
"settings.nav.globalVariables": "Variáveis globais",
"settings.nav.mcpClient": "Cliente Langflow MCP",
@@ -1672,6 +1673,8 @@
"settings.passwordTitle": "Senha",
"settings.profilePictureCategory.People": "Pessoas",
"settings.profilePictureCategory.Space": "Espaço",
+ "settings.avatarAlt": "avatar {{folder}} {{index}}",
+ "navigation.goBack": "Voltar",
"settings.profilePictureDescription": "Escolha a imagem que aparecerá como sua foto de perfil.",
"settings.profilePictureTitle": "Foto de perfil",
"settings.recordingKeyboard": "Gravando o teclado",
diff --git a/src/frontend/src/locales/zh-Hans.json b/src/frontend/src/locales/zh-Hans.json
index a581fc03a5..78fada7dc3 100644
--- a/src/frontend/src/locales/zh-Hans.json
+++ b/src/frontend/src/locales/zh-Hans.json
@@ -1656,6 +1656,7 @@
"settings.mcpClient.title": "Langflow MCP 客户端",
"settings.nav.apiKeys": "Langflow API 密钥",
"settings.nav.dbProviders": "数据库提供商",
+ "settings.nav.label": "设置导航",
"settings.nav.general": "常规",
"settings.nav.globalVariables": "全局变量",
"settings.nav.mcpClient": "Langflow MCP 客户端",
@@ -1672,6 +1673,8 @@
"settings.passwordTitle": "密码",
"settings.profilePictureCategory.People": "人员",
"settings.profilePictureCategory.Space": "空间",
+ "settings.avatarAlt": "{{folder}}头像{{index}}",
+ "navigation.goBack": "返回",
"settings.profilePictureDescription": "选择要作为个人头像显示的图片。",
"settings.profilePictureTitle": "头像",
"settings.recordingKeyboard": "录制键盘",
diff --git a/src/frontend/src/pages/FlowPage/components/TraceComponent/TraceDetailView.tsx b/src/frontend/src/pages/FlowPage/components/TraceComponent/TraceDetailView.tsx
index 7eff898825..4ef2589507 100644
--- a/src/frontend/src/pages/FlowPage/components/TraceComponent/TraceDetailView.tsx
+++ b/src/frontend/src/pages/FlowPage/components/TraceComponent/TraceDetailView.tsx
@@ -102,8 +102,6 @@ export function TraceDetailView({ traceId, flowName }: TraceDetailViewProps) {
);
}
- const headerTitle = `${trace.name || flowName || "Trace"}`;
-
return (
({
+ __esModule: true,
+ useTranslation: () => ({
+ t: (key: string, opts?: Record
) => {
+ if (key === "settings.avatarAlt" && opts) {
+ return `${opts.folder} avatar ${opts.index}`;
+ }
+ return key;
+ },
+ }),
+ initReactI18next: {
+ type: "3rdParty",
+ init: () => {},
+ },
+}));
+
+jest.mock("@/customization/utils/custom-pre-load-image-url", () => ({
+ customPreLoadImageUrl: (path: string) =>
+ `/api/v1/files/profile_pictures/${path}`,
+}));
+
+jest.mock("@/stores/darkStore", () => ({
+ useDarkStore: (selector: (s: { dark: boolean }) => unknown) =>
+ selector({ dark: false }),
+}));
+
+jest.mock("../hooks/use-preload-images", () => {
+ const { useEffect } = require("react");
+ return {
+ __esModule: true,
+ default: (setImagesLoaded: (v: boolean) => void) => {
+ useEffect(() => {
+ setImagesLoaded(true);
+ }, []);
+ },
+ };
+});
+
+jest.mock("@/components/ui/loading", () => ({
+ __esModule: true,
+ default: () => ,
+}));
+
+jest.mock("@/components/ui/button", () => ({
+ Button: ({
+ children,
+ onClick,
+ unstyled: _unstyled,
+ ...rest
+ }: React.ButtonHTMLAttributes & {
+ unstyled?: boolean;
+ children: React.ReactNode;
+ }) => (
+
+ ),
+}));
+
+import ProfilePictureChooserComponent from "../index";
+
+const PROFILE_PICTURES = {
+ files: [],
+ People: ["avatar-01.svg", "avatar-02.svg"],
+ Space: ["space-01.svg"],
+};
+
+describe("ProfilePictureChooserComponent", () => {
+ it("shows loading spinner when loading prop is true", () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+
+ it("renders images with src URLs built from customPreLoadImageUrl", async () => {
+ render(
+ ,
+ );
+ await screen.findAllByRole("button");
+ expect(screen.getByAltText("People avatar 1")).toHaveAttribute(
+ "src",
+ "/api/v1/files/profile_pictures/People/avatar-01.svg",
+ );
+ expect(screen.getByAltText("Space avatar 1")).toHaveAttribute(
+ "src",
+ "/api/v1/files/profile_pictures/Space/space-01.svg",
+ );
+ });
+
+ it("renders folder category labels when loaded", async () => {
+ render(
+ ,
+ );
+ await screen.findByText("settings.profilePictureCategory.People");
+ expect(
+ screen.getByText("settings.profilePictureCategory.Space"),
+ ).toBeInTheDocument();
+ });
+
+ it("renders one button per avatar path", async () => {
+ render(
+ ,
+ );
+ const buttons = await screen.findAllByRole("button");
+ // 2 People + 1 Space = 3
+ expect(buttons).toHaveLength(3);
+ });
+
+ it("each image has a descriptive alt attribute", async () => {
+ render(
+ ,
+ );
+ await screen.findAllByRole("button");
+ expect(screen.getByAltText("People avatar 1")).toBeInTheDocument();
+ expect(screen.getByAltText("People avatar 2")).toBeInTheDocument();
+ expect(screen.getByAltText("Space avatar 1")).toBeInTheDocument();
+ });
+
+ it("selected button has aria-pressed=true, others have aria-pressed=false", async () => {
+ render(
+ ,
+ );
+ const buttons = await screen.findAllByRole("button");
+ expect(buttons[0]).toHaveAttribute("aria-pressed", "true");
+ expect(buttons[1]).toHaveAttribute("aria-pressed", "false");
+ expect(buttons[2]).toHaveAttribute("aria-pressed", "false");
+ });
+
+ it("all buttons have aria-pressed=false when no value is selected", async () => {
+ render(
+ ,
+ );
+ const buttons = await screen.findAllByRole("button");
+ buttons.forEach((btn) => {
+ expect(btn).toHaveAttribute("aria-pressed", "false");
+ });
+ });
+
+ it("calls onChange with the correct folder/path key when clicked", async () => {
+ const user = userEvent.setup();
+ const onChange = jest.fn();
+ render(
+ ,
+ );
+ const buttons = await screen.findAllByRole("button");
+ await user.click(buttons[1]);
+ expect(onChange).toHaveBeenCalledWith("People/avatar-02.svg");
+ });
+
+ it("calls onChange once per click", async () => {
+ const user = userEvent.setup();
+ const onChange = jest.fn();
+ render(
+ ,
+ );
+ const buttons = await screen.findAllByRole("button");
+ await user.click(buttons[0]);
+ expect(onChange).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not render buttons when profilePictures is empty", async () => {
+ render(
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+ });
+
+ it("renders buttons from all folders when multiple folders are present", async () => {
+ const multiFolder = {
+ files: [],
+ People: ["a.svg", "b.svg", "c.svg"],
+ Space: ["x.svg", "y.svg"],
+ };
+ render(
+ ,
+ );
+ const buttons = await screen.findAllByRole("button");
+ expect(buttons).toHaveLength(5);
+ });
+
+ it("does not show buttons while loading even when profilePictures is provided", () => {
+ render(
+ ,
+ );
+ expect(screen.queryAllByRole("button")).toHaveLength(0);
+ });
+});
diff --git a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx
index 8aebeceb7a..233fba003e 100644
--- a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx
+++ b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx
@@ -55,10 +55,15 @@ export default function ProfilePictureChooserComponent({
unstyled
onClick={() => onChange(folder + "/" + path)}
className="shrink-0 px-0.5 py-2"
+ aria-pressed={value === folder + "/" + path}
>
})