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} >