From 1b9cd597405e093908d53b9f9258008d977ed482 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 8 Aug 2026 08:18:53 -0300 Subject: [PATCH] feat(dashboard): Modality Bridge settings page with vision/audio/video tabs + sidebar entry --- .../ModalityBridgeComingSoonTab.tsx | 22 ++++ .../settings/modality-bridge/page.tsx | 83 +++++++++++++ .../(dashboard)/dashboard/settings/page.tsx | 2 + src/shared/constants/sidebarVisibility.ts | 3 + .../constants/sidebarVisibility/sections.ts | 7 ++ .../constants/sidebarVisibility/types.ts | 1 + tests/unit/ui/modality-bridge-page.test.tsx | 116 ++++++++++++++++++ 7 files changed, 234 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeComingSoonTab.tsx create mode 100644 src/app/(dashboard)/dashboard/settings/modality-bridge/page.tsx create mode 100644 tests/unit/ui/modality-bridge-page.test.tsx diff --git a/src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeComingSoonTab.tsx b/src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeComingSoonTab.tsx new file mode 100644 index 0000000000..929b9f9e71 --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeComingSoonTab.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +interface ModalityBridgeComingSoonTabProps { + bodyKey: string; +} + +export default function ModalityBridgeComingSoonTab({ bodyKey }: ModalityBridgeComingSoonTabProps) { + const t = useTranslations("settings"); + + return ( +
+
+ +

{t(bodyKey)}

+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/settings/modality-bridge/page.tsx b/src/app/(dashboard)/dashboard/settings/modality-bridge/page.tsx new file mode 100644 index 0000000000..6d29768751 --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/modality-bridge/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { Suspense, useMemo } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import ModalityBridgeComingSoonTab from "@/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeComingSoonTab"; +import ModalityBridgeVisionTab from "@/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab"; + +type TabId = "vision" | "audio" | "video"; + +const TABS: ReadonlyArray<{ id: TabId; labelKey: string; fallback: string }> = [ + { id: "vision", labelKey: "modalityBridgeVisionTab", fallback: "Vision" }, + { id: "audio", labelKey: "modalityBridgeAudioTab", fallback: "Audio" }, + { id: "video", labelKey: "modalityBridgeVideoTab", fallback: "Video" }, +]; + +function ModalityBridgePageContent() { + const t = useTranslations("settings"); + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + const translateOrFallback = (key: string, fallback: string) => + typeof t.has === "function" && !t.has(key) ? fallback : t(key); + + const activeTab = useMemo(() => { + const requested = searchParams.get("tab") as TabId | null; + return requested && TABS.some((tab) => tab.id === requested) ? requested : "vision"; + }, [searchParams]); + + const handleTabChange = (tab: TabId) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("tab", tab); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + }; + + return ( +
+

{t("modalityBridgeIntro")}

+
+ {TABS.map((tab) => ( + + ))} +
+ +
+ {activeTab === "vision" && } + {activeTab === "audio" && ( + + )} + {activeTab === "video" && ( + + )} +
+
+ ); +} + +export default function ModalityBridgePage() { + return ( + + + + ); +} diff --git a/src/app/(dashboard)/dashboard/settings/page.tsx b/src/app/(dashboard)/dashboard/settings/page.tsx index c9dba29467..10a1f709a8 100644 --- a/src/app/(dashboard)/dashboard/settings/page.tsx +++ b/src/app/(dashboard)/dashboard/settings/page.tsx @@ -8,6 +8,8 @@ const LEGACY_TAB_ROUTES: Record = { "feature-flags": "/dashboard/settings/feature-flags", cache: "/dashboard/settings/cache", general: "/dashboard/settings/general", + modalityBridge: "/dashboard/settings/modality-bridge", + "modality-bridge": "/dashboard/settings/modality-bridge", resilience: "/dashboard/settings/resilience", routing: "/dashboard/settings/routing", security: "/dashboard/settings/security", diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index c2797452f6..7e12fdd479 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -68,6 +68,7 @@ export const SIDEBAR_ICON_ACCENTS: Partial> = { "settings-general": "#64748B", "settings-appearance": "#D946EF", "settings-ai": "#A78BFA", + "settings-modality-bridge": "#8B5CF6", "settings-routing": "#06B6D4", "settings-resilience": "#22C55E", "settings-advanced": "#F97316", @@ -201,6 +202,7 @@ const DEVELOPER_SHOWN: ReadonlySet = new Set([ "mcp", "a2a", "settings-general", + "settings-modality-bridge", "settings-routing", "settings-resilience", "settings-sidebar", @@ -232,6 +234,7 @@ const ADMIN_SHOWN: ReadonlySet = new Set([ "audit-mcp", "audit-a2a", "settings-general", + "settings-modality-bridge", "settings-routing", "settings-resilience", "settings-security", diff --git a/src/shared/constants/sidebarVisibility/sections.ts b/src/shared/constants/sidebarVisibility/sections.ts index 2609b08cde..bbf2f2668f 100644 --- a/src/shared/constants/sidebarVisibility/sections.ts +++ b/src/shared/constants/sidebarVisibility/sections.ts @@ -668,6 +668,13 @@ const CONFIGURATION_ITEMS: readonly SidebarItemDefinition[] = [ subtitleKey: "settingsAiSubtitle", icon: "auto_awesome", }, + { + id: "settings-modality-bridge", + href: "/dashboard/settings/modality-bridge", + i18nKey: "settingsModalityBridge", + subtitleKey: "settingsModalityBridgeSubtitle", + icon: "image_search", + }, { id: "settings-routing", href: "/dashboard/settings/routing", diff --git a/src/shared/constants/sidebarVisibility/types.ts b/src/shared/constants/sidebarVisibility/types.ts index b8a5b807e6..89e78b6359 100644 --- a/src/shared/constants/sidebarVisibility/types.ts +++ b/src/shared/constants/sidebarVisibility/types.ts @@ -93,6 +93,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [ "settings-general", "settings-appearance", "settings-ai", + "settings-modality-bridge", "settings-routing", "settings-resilience", "settings-advanced", diff --git a/tests/unit/ui/modality-bridge-page.test.tsx b/tests/unit/ui/modality-bridge-page.test.tsx new file mode 100644 index 0000000000..2a4f04f255 --- /dev/null +++ b/tests/unit/ui/modality-bridge-page.test.tsx @@ -0,0 +1,116 @@ +// @vitest-environment jsdom +import React, { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import ModalityBridgePage from "@/app/(dashboard)/dashboard/settings/modality-bridge/page"; +import SettingsPage from "@/app/(dashboard)/dashboard/settings/page"; +import { + getSectionItems, + SIDEBAR_PRESETS, + SIDEBAR_SECTIONS, +} from "@/shared/constants/sidebarVisibility"; + +const navigation = vi.hoisted(() => ({ + search: "", + replace: vi.fn(), + redirect: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ replace: navigation.replace }), + useSearchParams: () => new URLSearchParams(navigation.search), + usePathname: () => "/dashboard/settings/modality-bridge", + redirect: navigation.redirect, +})); + +vi.mock("next-intl", () => ({ + useTranslations: () => Object.assign((key: string) => key, { has: () => true }), +})); + +vi.mock( + "@/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab", + () => ({ default: () =>
vision
}) +); + +const roots: Array<{ root: Root; el: HTMLDivElement }> = []; + +function renderPage(): HTMLDivElement { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + act(() => root.render()); + roots.push({ root, el }); + return el; +} + +describe("Modality Bridge settings page", () => { + beforeEach(() => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + navigation.search = ""; + navigation.replace.mockReset(); + navigation.redirect.mockReset(); + }); + + afterEach(() => { + for (const { root, el } of roots.splice(0)) { + act(() => root.unmount()); + el.remove(); + } + }); + + it("renders the audio deep-link with all three modality tabs", () => { + navigation.search = "tab=audio"; + const el = renderPage(); + expect(el.querySelectorAll('[role="tab"]')).toHaveLength(3); + expect(el.textContent).toContain("modalityBridgeAudioComingSoon"); + expect(el.querySelector('[role="tab"][aria-selected="true"]')?.textContent).toContain( + "modalityBridgeAudioTab" + ); + }); + + it("defaults invalid or missing tab values to Vision", () => { + navigation.search = "tab=unsupported"; + const el = renderPage(); + expect(el.querySelector('[data-testid="vision-tab"]')).toBeTruthy(); + }); + + it("updates the URL without scrolling when an operator changes tabs", () => { + navigation.search = "tab=vision&source=sidebar"; + const el = renderPage(); + const videoTab = Array.from(el.querySelectorAll('[role="tab"]')).find((button) => + button.textContent?.includes("modalityBridgeVideoTab") + ) as HTMLButtonElement | undefined; + act(() => videoTab?.click()); + expect(navigation.replace).toHaveBeenCalledWith( + "/dashboard/settings/modality-bridge?tab=video&source=sidebar", + { scroll: false } + ); + }); + + it("redirects both legacy tab spellings to the dedicated page", async () => { + await SettingsPage({ searchParams: Promise.resolve({ tab: "modality-bridge" }) }); + await SettingsPage({ searchParams: Promise.resolve({ tab: "modalityBridge" }) }); + expect(navigation.redirect).toHaveBeenNthCalledWith(1, "/dashboard/settings/modality-bridge"); + expect(navigation.redirect).toHaveBeenNthCalledWith(2, "/dashboard/settings/modality-bridge"); + }); + + it("registers the sidebar item and exposes it in developer/admin presets", () => { + const section = SIDEBAR_SECTIONS.find((candidate) => candidate.id === "configuration"); + expect(section).toBeTruthy(); + const item = section + ? getSectionItems(section).find((candidate) => candidate.id === "settings-modality-bridge") + : undefined; + expect(item).toMatchObject({ + href: "/dashboard/settings/modality-bridge", + i18nKey: "settingsModalityBridge", + subtitleKey: "settingsModalityBridgeSubtitle", + icon: "image_search", + }); + + for (const presetId of ["developer", "admin"] as const) { + const preset = SIDEBAR_PRESETS.find((candidate) => candidate.id === presetId); + expect(preset?.hiddenItems).not.toContain("settings-modality-bridge"); + } + }); +});