mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
feat(frontend): add automations button to sidebar (#13941)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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]">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "会話",
|
||||
|
||||
21
frontend/src/icons/automations.svg
Normal file
21
frontend/src/icons/automations.svg
Normal 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 |
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user