feat(frontend): add automations button to sidebar (#13941)

This commit is contained in:
Hiep Le
2026-04-16 01:34:55 +07:00
committed by GitHub
parent d58106b29b
commit dcf044f8c3
8 changed files with 150 additions and 0 deletions

View File

@@ -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<typeof vi.spyOn>;
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();
});
});
});

View File

@@ -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(<AutomationsButton />);
const link = screen.getByTestId("automations-button");
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "/automations");
});
it("should be focusable and accessible when enabled", () => {
render(<AutomationsButton />);
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(<AutomationsButton disabled />);
const link = screen.getByTestId("automations-button");
expect(link).toHaveAttribute("tabIndex", "-1");
const clickEvent = createEvent.click(link);
fireEvent(link, clickEvent);
expect(clickEvent.defaultPrevented).toBe(true);
});
});

View File

@@ -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() && (
<AutomationsButton
disabled={settings?.email_verified === false}
/>
)}
</div>
<div className="flex flex-row md:flex-col md:items-center gap-[26px]">

View File

@@ -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 (
<StyledTooltip content={label} placement="right">
<a
href="/automations"
data-testid="automations-button"
aria-label={label}
tabIndex={disabled ? -1 : 0}
onClick={(e) => {
if (disabled) {
e.preventDefault();
}
}}
className={cn("inline-flex items-center justify-center", {
"pointer-events-none opacity-50": disabled,
})}
>
<AutomationsIcon width={24} height={24} />
</a>
</StyledTooltip>
);
}

View File

@@ -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",

View File

@@ -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": "会話",

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 20 20" fill="none">
<mask id="mask0_18339_1445" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="1" y="1" width="18" height="18">
<path d="M19 1H1V19H19V1Z" fill="white"/>
</mask>
<g mask="url(#mask0_18339_1445)">
<path d="M10 18.1818V16.5454" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 1.81836V3.45472" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.81824 10H3.4546" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.1818 10H16.5454" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.93359 17.1019L6.74359 15.6782" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.0663 2.89844L13.2563 4.32208" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.89819 5.93359L4.32183 6.74359" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.1019 14.0663L15.6782 13.2563" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.92542 2.90625L6.7436 4.3217" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.0909 17.0854L13.2727 15.6699" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.0855 5.90918L15.67 6.72736" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91455 14.0911L4.33001 13.2729" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 16.5455C13.615 16.5455 16.5455 13.615 16.5455 10C16.5455 6.38509 13.615 3.45459 10 3.45459C6.38509 3.45459 3.45459 6.38509 3.45459 10C3.45459 13.615 6.38509 16.5455 10 16.5455Z" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5854 9.77916L8.80545 7.59461C8.63363 7.49643 8.41272 7.61916 8.41272 7.81552V12.1846C8.41272 12.381 8.62545 12.5119 8.80545 12.4055L12.5854 10.221C12.7573 10.1228 12.7573 9.86916 12.5854 9.77098V9.77916Z" fill="currentColor" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -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");