diff --git a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx index cf6ce1ff9b..4f9249592c 100644 --- a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx +++ b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx @@ -11,6 +11,7 @@ import OptionService from "#/api/option-service/option-service.api"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; import { WebClientConfig } from "#/api/option-service/option.types"; import { useSelectedOrganizationStore } from "#/stores/selected-organization-store"; +import * as FeatureFlags from "#/utils/feature-flags"; // Helper to create mock config with sensible defaults const createMockConfig = ( @@ -185,4 +186,36 @@ describe("Sidebar", () => { }); }); }); + + describe("Automations button visibility", () => { + let enableAutomationsSpy: ReturnType; + + beforeEach(() => { + enableAutomationsSpy = vi.spyOn(FeatureFlags, "ENABLE_AUTOMATIONS"); + }); + + it("should show automations button when ENABLE_AUTOMATIONS flag is on", async () => { + enableAutomationsSpy.mockReturnValue(true); + + renderSidebar(); + + await waitFor(() => { + expect( + screen.getByTestId("automations-button"), + ).toBeInTheDocument(); + }); + }); + + it("should hide automations button when ENABLE_AUTOMATIONS flag is off", async () => { + enableAutomationsSpy.mockReturnValue(false); + + renderSidebar(); + + await waitFor(() => expect(getSettingsSpy).toHaveBeenCalled()); + + expect( + screen.queryByTestId("automations-button"), + ).not.toBeInTheDocument(); + }); + }); }); diff --git a/frontend/__tests__/components/shared/buttons/automations-button.test.tsx b/frontend/__tests__/components/shared/buttons/automations-button.test.tsx new file mode 100644 index 0000000000..e50c601d5e --- /dev/null +++ b/frontend/__tests__/components/shared/buttons/automations-button.test.tsx @@ -0,0 +1,32 @@ +import { createEvent, fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { AutomationsButton } from "#/components/shared/buttons/automations-button"; + +describe("AutomationsButton", () => { + it("should render a link to /automations", () => { + render(); + + const link = screen.getByTestId("automations-button"); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "/automations"); + }); + + it("should be focusable and accessible when enabled", () => { + render(); + + const link = screen.getByTestId("automations-button"); + expect(link).toHaveAttribute("tabIndex", "0"); + expect(link).toHaveAttribute("aria-label", "SIDEBAR$AUTOMATIONS"); + }); + + it("should prevent navigation and remove from tab order when disabled", () => { + render(); + + const link = screen.getByTestId("automations-button"); + expect(link).toHaveAttribute("tabIndex", "-1"); + + const clickEvent = createEvent.click(link); + fireEvent(link, clickEvent); + expect(clickEvent.defaultPrevented).toBe(true); + }); +}); diff --git a/frontend/src/components/features/sidebar/sidebar.tsx b/frontend/src/components/features/sidebar/sidebar.tsx index 258eda9e30..ce32ec7e20 100644 --- a/frontend/src/components/features/sidebar/sidebar.tsx +++ b/frontend/src/components/features/sidebar/sidebar.tsx @@ -6,6 +6,7 @@ import { UserActions } from "./user-actions"; import { OpenHandsLogoButton } from "#/components/shared/buttons/openhands-logo-button"; import { NewProjectButton } from "#/components/shared/buttons/new-project-button"; import { ConversationPanelButton } from "#/components/shared/buttons/conversation-panel-button"; +import { AutomationsButton } from "#/components/shared/buttons/automations-button"; import { SettingsModal } from "#/components/shared/modals/settings/settings-modal"; import { useSettings } from "#/hooks/query/use-settings"; import { ConversationPanel } from "../conversation-panel/conversation-panel"; @@ -14,6 +15,7 @@ import { useConfig } from "#/hooks/query/use-config"; import { displayErrorToast } from "#/utils/custom-toast-handlers"; import { I18nKey } from "#/i18n/declaration"; import { cn } from "#/utils/utils"; +import { ENABLE_AUTOMATIONS } from "#/utils/feature-flags"; export function Sidebar() { const { t } = useTranslation(); @@ -87,6 +89,11 @@ export function Sidebar() { } disabled={settings?.email_verified === false} /> + {ENABLE_AUTOMATIONS() && ( + + )}
diff --git a/frontend/src/components/shared/buttons/automations-button.tsx b/frontend/src/components/shared/buttons/automations-button.tsx new file mode 100644 index 0000000000..e1f7bfc975 --- /dev/null +++ b/frontend/src/components/shared/buttons/automations-button.tsx @@ -0,0 +1,38 @@ +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; +import { StyledTooltip } from "#/components/shared/buttons/styled-tooltip"; +import AutomationsIcon from "#/icons/automations.svg?react"; +import { cn } from "#/utils/utils"; + +interface AutomationsButtonProps { + disabled?: boolean; +} + +export function AutomationsButton({ + disabled = false, +}: AutomationsButtonProps) { + const { t } = useTranslation(); + + const label = t(I18nKey.SIDEBAR$AUTOMATIONS); + + return ( + + { + if (disabled) { + e.preventDefault(); + } + }} + className={cn("inline-flex items-center justify-center", { + "pointer-events-none opacity-50": disabled, + })} + > + + + + ); +} diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index f96a499299..c303833201 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -470,6 +470,7 @@ export enum I18nKey { SIDEBAR$NAVIGATION_LABEL = "SIDEBAR$NAVIGATION_LABEL", FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL", FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL", + SIDEBAR$AUTOMATIONS = "SIDEBAR$AUTOMATIONS", SIDEBAR$CONVERSATIONS = "SIDEBAR$CONVERSATIONS", STATUS$CONNECTING_TO_RUNTIME = "STATUS$CONNECTING_TO_RUNTIME", STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 371838298c..025c74ff5d 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -7989,6 +7989,23 @@ "uk": "Приватний", "ca": "Privat" }, + "SIDEBAR$AUTOMATIONS": { + "en": "Automations", + "zh-CN": "自动化", + "zh-TW": "自動化", + "de": "Automatisierungen", + "ko-KR": "자동화", + "no": "Automatiseringer", + "it": "Automazioni", + "pt": "Automatizações", + "es": "Automatizaciones", + "ar": "الأتمتة", + "fr": "Automatisations", + "tr": "Otomasyonlar", + "ja": "自動化", + "uk": "Автоматизації", + "ca": "Automatitzacions" + }, "SIDEBAR$CONVERSATIONS": { "en": "Conversations", "ja": "会話", diff --git a/frontend/src/icons/automations.svg b/frontend/src/icons/automations.svg new file mode 100644 index 0000000000..62c57aa7cc --- /dev/null +++ b/frontend/src/icons/automations.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/utils/feature-flags.ts b/frontend/src/utils/feature-flags.ts index 442acba50d..aaaf2c5d8b 100644 --- a/frontend/src/utils/feature-flags.ts +++ b/frontend/src/utils/feature-flags.ts @@ -22,3 +22,4 @@ export const ENABLE_PROJ_USER_JOURNEY = () => loadFeatureFlag("PROJ_USER_JOURNEY"); export const ENABLE_SANDBOX_GROUPING = () => loadFeatureFlag("SANDBOX_GROUPING"); +export const ENABLE_AUTOMATIONS = () => loadFeatureFlag("AUTOMATIONS");