diff --git a/__tests__/api/option-service.test.ts b/__tests__/api/option-service.test.ts index 11a32b1baa..cad105e2c8 100644 --- a/__tests__/api/option-service.test.ts +++ b/__tests__/api/option-service.test.ts @@ -19,7 +19,8 @@ describe("OptionService", () => { it("returns config in mock mode without a live backend", async () => { const config = await OptionService.getConfig(); - expect(config.feature_flags.hide_integrations_page).toBe(false); + expect(config.feature_flags.hide_llm_settings).toBe(false); + expect(config.feature_flags.hide_users_page).toBe(true); expect(config.updated_at).toBeTruthy(); }); @@ -66,7 +67,7 @@ describe("OptionService", () => { await expect(OptionService.getConfig()).resolves.toMatchObject({ feature_flags: expect.objectContaining({ - hide_integrations_page: false, + hide_llm_settings: false, }), }); }); diff --git a/__tests__/api/settings-service.test.ts b/__tests__/api/settings-service.test.ts index 0a5bbf3bf1..737cab1fcf 100644 --- a/__tests__/api/settings-service.test.ts +++ b/__tests__/api/settings-service.test.ts @@ -128,8 +128,8 @@ describe("SettingsService", () => { // Arrange: simulate post-save state. SecretsService.addGitProvider writes to // this localStorage key after the server PUT /api/settings/secrets succeeds. // The agent-server API never returns provider_tokens_set, so the GUI must - // derive it from local state for useUserProviders / git-settings to recognize - // the configured providers. + // derive it from local state for useUserProviders to recognize the + // configured providers. window.localStorage.setItem( "openhands-agent-server-git-provider-tokens", JSON.stringify({ diff --git a/__tests__/components/features/home/repo-connector.test.tsx b/__tests__/components/features/home/repo-connector.test.tsx index f5133b05aa..204469cc51 100644 --- a/__tests__/components/features/home/repo-connector.test.tsx +++ b/__tests__/components/features/home/repo-connector.test.tsx @@ -24,10 +24,6 @@ vi.mock("#/components/features/home/workspace-selection-form", () => ({ WorkspaceSelectionForm: () =>
, })); -vi.mock("#/components/features/home/connect-to-provider-message", () => ({ - ConnectToProviderMessage: () =>
, -})); - const cloudBackend: Backend = { id: "prod", name: "Production", @@ -78,7 +74,7 @@ describe("RepoConnector", () => { ).not.toBeInTheDocument(); }); - it("shows the connect-provider message for cloud backends without providers", () => { + it("does not render the repository launcher for cloud backends without providers", () => { setRegisteredBackends([cloudBackend]); setActiveSelection({ backendId: cloudBackend.id }); mockUseUserProviders.mockReturnValue({ @@ -88,7 +84,6 @@ describe("RepoConnector", () => { renderRepoConnector(); - expect(screen.getByTestId("stub-connect-message")).toBeInTheDocument(); expect(screen.queryByTestId("stub-repo-form")).not.toBeInTheDocument(); expect( screen.queryByTestId("stub-workspace-form"), diff --git a/__tests__/components/features/home/task-suggestions.test.tsx b/__tests__/components/features/home/task-suggestions.test.tsx index 1dffe4c284..96ee054bff 100644 --- a/__tests__/components/features/home/task-suggestions.test.tsx +++ b/__tests__/components/features/home/task-suggestions.test.tsx @@ -35,11 +35,15 @@ vi.mock("#/hooks/query/use-config", () => ({ }), })); -vi.mock("#/hooks/use-user-providers", () => ({ - useUserProviders: () => ({ +const { useUserProvidersMock } = vi.hoisted(() => ({ + useUserProvidersMock: vi.fn(() => ({ providers: [{ id: "github", name: "GitHub" }], isLoading: false, - }), + })), +})); + +vi.mock("#/hooks/use-user-providers", () => ({ + useUserProviders: () => useUserProvidersMock(), })); const renderTaskSuggestions = () => { @@ -88,6 +92,21 @@ describe("TaskSuggestions", () => { await screen.findByText("TASKS$NO_TASKS_AVAILABLE"); }); + it("falls back to the generic empty message when no Git providers are connected and exposes no link to the removed Integrations page", async () => { + useUserProvidersMock.mockReturnValueOnce({ + providers: [], + isLoading: false, + }); + getSuggestedTasksSpy.mockResolvedValue([]); + + renderTaskSuggestions(); + + await screen.findByText("TASKS$NO_TASKS_AVAILABLE"); + expect( + screen.queryByRole("link", { name: /integrations/i }), + ).not.toBeInTheDocument(); + }); + it("should render the task groups with the correct titles", async () => { getSuggestedTasksSpy.mockResolvedValue(MOCK_TASKS); renderTaskSuggestions(); diff --git a/__tests__/components/features/settings/webhook-status-badge.test.tsx b/__tests__/components/features/settings/webhook-status-badge.test.tsx deleted file mode 100644 index 361c0d2590..0000000000 --- a/__tests__/components/features/settings/webhook-status-badge.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import { WebhookStatusBadge } from "#/components/features/settings/git-settings/webhook-status-badge"; - -// Mock react-i18next -vi.mock("react-i18next", () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -describe("WebhookStatusBadge", () => { - it("should display installed status when webhook is installed", () => { - // Arrange - const props = { - webhookInstalled: true, - }; - - // Act - render(); - - // Assert - const badge = screen.getByText("GITLAB$WEBHOOK_STATUS_INSTALLED"); - expect(badge).toBeInTheDocument(); - }); - - it("should display not installed status when webhook is not installed", () => { - // Arrange - const props = { - webhookInstalled: false, - }; - - // Act - render(); - - // Assert - const badge = screen.getByText("GITLAB$WEBHOOK_STATUS_NOT_INSTALLED"); - expect(badge).toBeInTheDocument(); - }); - - it("should display installed status when installation result is successful", () => { - // Arrange - const props = { - webhookInstalled: false, - installationResult: { - success: true, - error: null, - }, - }; - - // Act - render(); - - // Assert - const badge = screen.getByText("GITLAB$WEBHOOK_STATUS_INSTALLED"); - expect(badge).toBeInTheDocument(); - }); - - it("should display failed status when installation result has error", () => { - // Arrange - const props = { - webhookInstalled: false, - installationResult: { - success: false, - error: "Installation failed", - }, - }; - - // Act - render(); - - // Assert - const badge = screen.getByText("GITLAB$WEBHOOK_STATUS_FAILED"); - expect(badge).toBeInTheDocument(); - }); - - it("should show error message when installation fails", () => { - // Arrange - const errorMessage = "Permission denied"; - const props = { - webhookInstalled: false, - installationResult: { - success: false, - error: errorMessage, - }, - }; - - // Act - render(); - - // Assert - const badgeContainer = screen.getByText( - "GITLAB$WEBHOOK_STATUS_FAILED", - ).parentElement; - expect(badgeContainer).toHaveAttribute("title", errorMessage); - }); -}); diff --git a/__tests__/components/shared/navigation-link.test.tsx b/__tests__/components/shared/navigation-link.test.tsx index 631602d63b..2a18628d56 100644 --- a/__tests__/components/shared/navigation-link.test.tsx +++ b/__tests__/components/shared/navigation-link.test.tsx @@ -20,7 +20,7 @@ function renderNavigationLink( const result = render( - Integrations + MCP , ); @@ -32,13 +32,13 @@ function renderNavigationLink( describe("NavigationLink", () => { it("renders the destination href and active state from navigation context", () => { - renderNavigationLink("/settings/integrations"); + renderNavigationLink("/settings/mcp"); - expect(screen.getByRole("link", { name: "Integrations" })).toHaveAttribute( + expect(screen.getByRole("link", { name: "MCP" })).toHaveAttribute( "href", - "/settings/integrations", + "/settings/mcp", ); - expect(screen.getByRole("link", { name: "Integrations" })).toHaveAttribute( + expect(screen.getByRole("link", { name: "MCP" })).toHaveAttribute( "aria-current", "page", ); @@ -46,11 +46,11 @@ describe("NavigationLink", () => { it("uses the injected navigate callback on click", () => { const { navigate } = renderNavigationLink(); - const link = screen.getByRole("link", { name: "Integrations" }); + const link = screen.getByRole("link", { name: "MCP" }); fireEvent.click(link); - expect(navigate).toHaveBeenCalledWith("/settings/integrations", { + expect(navigate).toHaveBeenCalledWith("/settings/mcp", { replace: false, }); }); diff --git a/__tests__/components/user-actions.test.tsx b/__tests__/components/user-actions.test.tsx index c659dfcd71..98fe13e69e 100644 --- a/__tests__/components/user-actions.test.tsx +++ b/__tests__/components/user-actions.test.tsx @@ -10,7 +10,7 @@ import { ActiveBackendProvider } from "#/contexts/active-backend-context"; vi.mock("#/hooks/use-settings-nav-items", () => ({ useSettingsNavItems: () => [ { type: "item", item: OSS_NAV_ITEMS[0] }, - { type: "item", item: OSS_NAV_ITEMS[6] }, + { type: "item", item: OSS_NAV_ITEMS[5] }, ], })); diff --git a/__tests__/hooks/mutation/use-add-git-providers.test.tsx b/__tests__/hooks/mutation/use-add-git-providers.test.tsx deleted file mode 100644 index 6e8c88d9de..0000000000 --- a/__tests__/hooks/mutation/use-add-git-providers.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { renderHook } from "@testing-library/react"; -import React from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { SecretsService } from "#/api/secrets-service"; -import { useAddGitProviders } from "#/hooks/mutation/use-add-git-providers"; -import { Provider, ProviderToken } from "#/types/settings"; - -const mockTrackGitProviderConnected = vi.fn(); - -vi.mock("#/hooks/use-tracking", () => ({ - useTracking: () => ({ - trackGitProviderConnected: mockTrackGitProviderConnected, - }), -})); - -const buildProviders = ( - overrides: Partial> = {}, -): Record => ({ - github: { token: "", host: null }, - gitlab: { token: "", host: null }, - bitbucket: { token: "", host: null }, - bitbucket_data_center: { token: "", host: null }, - azure_devops: { token: "", host: null }, - forgejo: { token: "", host: null }, - ...overrides, -}); - -describe("useAddGitProviders", () => { - let queryClient: QueryClient; - - beforeEach(() => { - vi.restoreAllMocks(); - mockTrackGitProviderConnected.mockReset(); - queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, - }); - }); - - it("invalidates personal settings queries after saving providers", async () => { - vi.spyOn(SecretsService, "addGitProvider").mockResolvedValue(undefined); - - const personalSettingsQueryKey = ["settings", "personal"] as const; - queryClient.setQueryData(personalSettingsQueryKey, { - provider_tokens_set: {}, - }); - - const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); - - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - - const { result } = renderHook(() => useAddGitProviders(), { wrapper }); - - await result.current.mutateAsync({ - providers: buildProviders({ - github: { token: "ghp_test_123", host: null }, - }), - }); - - expect(invalidateSpy).toHaveBeenCalled(); - expect( - queryClient.getQueryState(personalSettingsQueryKey)?.isInvalidated, - ).toBe(true); - }); -}); diff --git a/__tests__/hooks/use-settings-nav-items.test.tsx b/__tests__/hooks/use-settings-nav-items.test.tsx index e0cab683cf..98f0acbf10 100644 --- a/__tests__/hooks/use-settings-nav-items.test.tsx +++ b/__tests__/hooks/use-settings-nav-items.test.tsx @@ -5,49 +5,18 @@ import { useSettingsNavItems } from "#/hooks/use-settings-nav-items"; import { WebClientConfig } from "#/api/option-service/option.types"; const useConfigMock = vi.fn(); -const useActiveBackendMock = vi.fn(); vi.mock("#/hooks/query/use-config", () => ({ useConfig: () => useConfigMock(), })); -vi.mock("#/contexts/active-backend-context", () => ({ - useActiveBackend: () => useActiveBackendMock(), -})); - -const localActive = { - backend: { - id: "__bundled__", - name: "Local", - host: "http://localhost", - apiKey: "", - kind: "local" as const, - }, - orgId: null, -}; - -const cloudActive = { - backend: { - id: "prod", - name: "Production", - host: "https://app.all-hands.dev", - apiKey: "bearer", - kind: "cloud" as const, - }, - orgId: null, -}; - const createConfig = ( feature_flags: Partial = {}, ): WebClientConfig => ({ posthog_client_key: null, feature_flags: { hide_llm_settings: false, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, hide_users_page: true, - hide_integrations_page: false, ...feature_flags, }, providers_configured: [], @@ -61,7 +30,6 @@ const createConfig = ( describe("useSettingsNavItems", () => { beforeEach(() => { vi.clearAllMocks(); - useActiveBackendMock.mockReturnValue(localActive); }); it("returns the OSS settings items in order", () => { @@ -76,10 +44,7 @@ describe("useSettingsNavItems", () => { it("filters hidden routes from the OSS settings items", () => { useConfigMock.mockReturnValue({ - data: createConfig({ - hide_llm_settings: true, - hide_integrations_page: true, - }), + data: createConfig({ hide_llm_settings: true }), }); const { result } = renderHook(() => useSettingsNavItems()); @@ -88,38 +53,18 @@ describe("useSettingsNavItems", () => { .map((item) => (item.type === "item" ? item.item.to : null)); expect(paths).not.toContain("/settings"); - expect(paths).not.toContain("/settings/integrations"); expect(paths).toContain("/settings/mcp"); expect(paths).toContain("/settings/secrets"); }); - it("never lists the removed /settings/agent-server entry", () => { + it("never lists the removed /settings/integrations entry", () => { useConfigMock.mockReturnValue({ data: createConfig() }); - const { result } = renderHook(() => useSettingsNavItems()); - const paths = result.current - .filter((item) => item.type === "item") - .map((item) => (item.type === "item" ? item.item.to : null)); - - expect(paths).not.toContain("/settings/agent-server"); - }); - - - it("hides local-only sub-pages when the active backend is cloud", () => { - useConfigMock.mockReturnValue({ data: createConfig() }); - useActiveBackendMock.mockReturnValue(cloudActive); - const { result } = renderHook(() => useSettingsNavItems()); const paths = result.current .filter((item) => item.type === "item") .map((item) => (item.type === "item" ? item.item.to : null)); expect(paths).not.toContain("/settings/integrations"); - expect(paths).toContain("/settings"); - expect(paths).toContain("/settings/condenser"); - expect(paths).toContain("/settings/verification"); - expect(paths).toContain("/settings/mcp"); - expect(paths).toContain("/settings/skills"); - expect(paths).toContain("/settings/secrets"); }); }); diff --git a/__tests__/library-entrypoints.test.ts b/__tests__/library-entrypoints.test.ts index 06c4cb6ae1..88ac7700e4 100644 --- a/__tests__/library-entrypoints.test.ts +++ b/__tests__/library-entrypoints.test.ts @@ -34,7 +34,6 @@ describe("library public entrypoints", () => { expect(filesApi.FileExplorer).toBeTypeOf("function"); expect(settingsApi.SettingsPanel).toBeTypeOf("function"); expect(settingsApi.AppSettings).toBeTypeOf("function"); - expect(settingsApi.GitSettings).toBeTypeOf("function"); expect(settingsApi.LLMSettings).toBeTypeOf("function"); expect(settingsApi.MCPSettings).toBeTypeOf("function"); expect(settingsApi.SecretsSettings).toBeTypeOf("function"); diff --git a/__tests__/router.md b/__tests__/router.md index 0a9ce48da9..4214543c92 100644 --- a/__tests__/router.md +++ b/__tests__/router.md @@ -52,14 +52,14 @@ const RouterStub = createRoutesStub([ path: "/settings", }, { - Component: () =>
, - path: "/settings/integrations", + Component: () =>
, + path: "/settings/mcp", }, ], }, ]); -render(); +render(); ``` > [!TIP] diff --git a/__tests__/routes/git-settings.test.tsx b/__tests__/routes/git-settings.test.tsx deleted file mode 100644 index 1c8fa7fce2..0000000000 --- a/__tests__/routes/git-settings.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import SettingsService from "#/api/settings-service/settings-service.api"; -import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; -import { GitSettingsScreen } from "#/routes/git-settings"; -import { Settings } from "#/types/settings"; - -function buildSettings(overrides: Partial = {}): Settings { - return { - ...MOCK_DEFAULT_USER_SETTINGS, - ...overrides, - provider_tokens_set: { - ...MOCK_DEFAULT_USER_SETTINGS.provider_tokens_set, - ...overrides.provider_tokens_set, - }, - agent_settings: { - ...MOCK_DEFAULT_USER_SETTINGS.agent_settings, - ...overrides.agent_settings, - }, - }; -} - -function renderGitSettingsScreen() { - return render(, { - wrapper: ({ children }) => ( - - {children} - - ), - }); -} - -describe("GitSettingsScreen", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("renders OSS git provider token inputs", async () => { - vi.spyOn(SettingsService, "getSettings").mockResolvedValue(buildSettings()); - - renderGitSettingsScreen(); - - const githubTokenInput = await screen.findByTestId("github-token-input"); - - expect(githubTokenInput).toBeInTheDocument(); - expect(screen.getByTestId("gitlab-token-input")).toBeInTheDocument(); - expect(screen.getByTestId("submit-button")).toBeDisabled(); - }); - - it("enables saving after a provider token changes", async () => { - vi.spyOn(SettingsService, "getSettings").mockResolvedValue(buildSettings()); - - renderGitSettingsScreen(); - - const user = userEvent.setup(); - const githubTokenInput = await screen.findByTestId("github-token-input"); - const submitButton = screen.getByTestId("submit-button"); - - expect(submitButton).toBeDisabled(); - - await user.type(githubTokenInput, "ghp_test_token"); - - expect(submitButton).toBeEnabled(); - }); - - it("enables disconnecting when a provider is already configured", async () => { - vi.spyOn(SettingsService, "getSettings").mockResolvedValue( - buildSettings({ - provider_tokens_set: { - github: "github.com", - }, - }), - ); - - renderGitSettingsScreen(); - - await waitFor(() => { - expect(screen.getByTestId("disconnect-tokens-button")).toBeEnabled(); - }); - }); -}); \ No newline at end of file diff --git a/__tests__/routes/root-layout-refetch.test.tsx b/__tests__/routes/root-layout-refetch.test.tsx index fab09e25c8..1f25667431 100644 --- a/__tests__/routes/root-layout-refetch.test.tsx +++ b/__tests__/routes/root-layout-refetch.test.tsx @@ -22,11 +22,7 @@ vi.mock("#/hooks/query/use-config", () => ({ const DEFAULT_FEATURE_FLAGS = { hide_llm_settings: false, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, hide_users_page: false, - hide_integrations_page: false, }; const RouterStub = createRoutesStub([ diff --git a/__tests__/routes/settings.test.tsx b/__tests__/routes/settings.test.tsx index 727cf45b6a..61b915e98d 100644 --- a/__tests__/routes/settings.test.tsx +++ b/__tests__/routes/settings.test.tsx @@ -4,12 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createRoutesStub } from "react-router"; import SettingsScreen, { clientLoader } from "#/routes/settings"; import OptionService from "#/api/option-service/option-service.api"; -import { - __resetActiveStoreForTests, - setActiveSelection, - setRegisteredBackends, -} from "#/api/backend-registry/active-store"; -import type { Backend } from "#/api/backend-registry/types"; +import { __resetActiveStoreForTests } from "#/api/backend-registry/active-store"; import { getFirstAvailablePath } from "#/utils/settings-utils"; import { OSS_NAV_ITEMS } from "#/constants/settings-nav"; import { ActiveBackendProvider } from "#/contexts/active-backend-context"; @@ -17,18 +12,10 @@ import { ActiveBackendProvider } from "#/contexts/active-backend-context"; vi.mock("#/hooks/use-settings-nav-items", () => ({ useSettingsNavItems: () => [ { type: "item", item: OSS_NAV_ITEMS[0] }, - { type: "item", item: OSS_NAV_ITEMS[6] }, + { type: "item", item: OSS_NAV_ITEMS[5] }, ], })); -const cloudBackend: Backend = { - id: "prod", - name: "Production", - host: "https://app.all-hands.dev", - apiKey: "bearer-token", - kind: "cloud", -}; - describe("settings route", () => { beforeEach(() => { vi.restoreAllMocks(); @@ -45,11 +32,7 @@ describe("settings route", () => { expect( getFirstAvailablePath({ hide_llm_settings: true, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, hide_users_page: true, - hide_integrations_page: false, }), ).toBe("/settings/mcp"); }); @@ -59,11 +42,7 @@ describe("settings route", () => { posthog_client_key: null, feature_flags: { hide_llm_settings: true, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, hide_users_page: true, - hide_integrations_page: false, }, providers_configured: [], maintenance_start_time: null, @@ -83,20 +62,28 @@ describe("settings route", () => { expect(response.headers.get("Location")).toBe("/settings/mcp"); }); - it("redirects local-only settings paths to /settings when the active backend is cloud", async () => { - setRegisteredBackends([cloudBackend]); - setActiveSelection({ backendId: cloudBackend.id }); - const getConfigSpy = vi.spyOn(OptionService, "getConfig"); + it("no longer treats /settings/integrations as a redirect target", async () => { + vi.spyOn(OptionService, "getConfig").mockResolvedValue({ + posthog_client_key: null, + feature_flags: { + hide_llm_settings: false, + hide_users_page: true, + }, + providers_configured: [], + maintenance_start_time: null, + recaptcha_site_key: null, + faulty_models: [], + error_message: null, + updated_at: new Date().toISOString(), + }); - const integrationsResponse = (await clientLoader({ + const result = await clientLoader({ request: new Request("http://localhost/settings/integrations"), params: {}, context: {}, - } as never)) as Response; + } as never); - expect(integrationsResponse.status).toBe(302); - expect(integrationsResponse.headers.get("Location")).toBe("/settings"); - expect(getConfigSpy).not.toHaveBeenCalled(); + expect(result).toBeNull(); }); it("renders the current OSS section title", () => { diff --git a/__tests__/task-suggestions.test.tsx b/__tests__/task-suggestions.test.tsx deleted file mode 100644 index 930e078b98..0000000000 --- a/__tests__/task-suggestions.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { MemoryRouter } from "react-router"; -import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions"; - -// Mock translation -vi.mock("react-i18next", () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -// Mock suggested tasks hook -vi.mock("#/hooks/query/use-suggested-tasks", () => ({ - useSuggestedTasks: () => ({ - data: [], - isLoading: false, - }), -})); - -// Mock config hook -vi.mock("#/hooks/query/use-config", () => ({ - useConfig: vi.fn(), -})); - -// Mock user providers hook -vi.mock("#/hooks/use-user-providers", () => ({ - useUserProviders: vi.fn(), -})); - -import { useConfig } from "#/hooks/query/use-config"; -import { useUserProviders } from "#/hooks/use-user-providers"; - -describe("TaskSuggestions empty states", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("OSS mode + no providers → shows Git provider empty state", () => { - (useConfig as any).mockReturnValue({ - data: {}, - }); - - (useUserProviders as any).mockReturnValue({ - providers: [], - }); - - render( - - - - ); - - expect(screen.getByText("TASKS$NO_GIT_PROVIDERS_TITLE")).toBeInTheDocument(); - expect(screen.getByText("TASKS$NO_GIT_PROVIDERS_DESCRIPTION")).toBeInTheDocument(); - expect(screen.getByText("TASKS$NO_GIT_PROVIDERS_CTA")).toBeInTheDocument(); - }); - - it("OSS mode + providers exist but no tasks → shows no tasks message", () => { - (useConfig as any).mockReturnValue({ - data: {}, - }); - - (useUserProviders as any).mockReturnValue({ - providers: [{ id: "github" }], - }); - - render( - - - - ); - - expect(screen.getByText("TASKS$NO_TASKS_AVAILABLE")).toBeInTheDocument(); - }); -}); diff --git a/src/api/option-service/option-service.api.ts b/src/api/option-service/option-service.api.ts index 4bd606a3ac..2245941750 100644 --- a/src/api/option-service/option-service.api.ts +++ b/src/api/option-service/option-service.api.ts @@ -37,11 +37,7 @@ class OptionService { posthog_client_key: null, feature_flags: { hide_llm_settings: false, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, hide_users_page: true, - hide_integrations_page: false, }, providers_configured: [], maintenance_start_time: null, diff --git a/src/api/option-service/option.types.ts b/src/api/option-service/option.types.ts index 9853a3846c..035ede1c1a 100644 --- a/src/api/option-service/option.types.ts +++ b/src/api/option-service/option.types.ts @@ -19,11 +19,7 @@ export interface ModelsResponse { export interface WebClientFeatureFlags { hide_llm_settings: boolean; - enable_jira: boolean; - enable_jira_dc: boolean; - enable_linear: boolean; hide_users_page: boolean; - hide_integrations_page: boolean; } export interface WebClientConfig { diff --git a/src/components/features/home/connect-to-provider-message.tsx b/src/components/features/home/connect-to-provider-message.tsx deleted file mode 100644 index fa77ecabd1..0000000000 --- a/src/components/features/home/connect-to-provider-message.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { BrandButton } from "#/components/features/settings/brand-button"; -import { NavigationLink } from "#/components/shared/navigation-link"; -import { useSettings } from "#/hooks/query/use-settings"; -import RepoForkedIcon from "#/icons/repo-forked.svg?react"; -import { I18nKey } from "#/i18n/declaration"; - -export function ConnectToProviderMessage() { - const { isLoading } = useSettings(); - const { t } = useTranslation("openhands"); - - return ( -
-
-
- - - {t(I18nKey.COMMON$OPEN_REPOSITORY)} - -
-

{t("HOME$CONNECT_PROVIDER_MESSAGE")}

-
- - - {!isLoading && t("SETTINGS$TITLE")} - {isLoading && t("HOME$LOADING")} - - -
- ); -} diff --git a/src/components/features/home/repo-connector.tsx b/src/components/features/home/repo-connector.tsx index 702fa2c19e..e6295f3e76 100644 --- a/src/components/features/home/repo-connector.tsx +++ b/src/components/features/home/repo-connector.tsx @@ -2,7 +2,6 @@ import { useActiveBackend } from "#/contexts/active-backend-context"; import { useUserProviders } from "#/hooks/use-user-providers"; import { GitRepository } from "#/types/git"; -import { ConnectToProviderMessage } from "./connect-to-provider-message"; import { RepositorySelectionForm } from "./repo-selection-form"; import { WorkspaceSelectionForm } from "./workspace-selection-form"; @@ -19,16 +18,13 @@ export function RepoConnector({ onRepoSelection }: RepoConnectorProps) { data-testid="repo-connector" className="w-full flex flex-col gap-6 rounded-[12px] p-[20px] border border-[#727987] bg-[#26282D] min-h-[263.5px] relative" > - {isCloud ? ( - providers.length > 0 ? ( - - ) : ( - - ) - ) : ( + {isCloud && providers.length > 0 && ( + + )} + {!isCloud && ( )} diff --git a/src/components/features/home/tasks/task-suggestions.tsx b/src/components/features/home/tasks/task-suggestions.tsx index d40ae1d1d4..c067821d4b 100644 --- a/src/components/features/home/tasks/task-suggestions.tsx +++ b/src/components/features/home/tasks/task-suggestions.tsx @@ -1,13 +1,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { TaskGroup } from "./task-group"; -import { NavigationLink } from "#/components/shared/navigation-link"; import { useSuggestedTasks } from "#/hooks/query/use-suggested-tasks"; import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton"; import { cn, getDisplayedTaskGroups, getTotalTaskCount } from "#/utils/utils"; import { I18nKey } from "#/i18n/declaration"; import { GitRepository } from "#/types/git"; -import { useUserProviders } from "#/hooks/use-user-providers"; import { Typography } from "#/ui/typography"; interface TaskSuggestionsProps { @@ -19,9 +17,6 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) { const [isExpanded, setIsExpanded] = useState(false); const { data: tasks, isLoading } = useSuggestedTasks(); - const { providers } = useUserProviders(); - - const hasNoProviders = providers.length === 0; const suggestedTasks = filterFor ? tasks?.filter( @@ -70,32 +65,11 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
)} - {!hasSuggestedTasks && - !isLoading && - (hasNoProviders ? ( -
- - {t(I18nKey.TASKS$NO_GIT_PROVIDERS_TITLE)} - - - - {t(I18nKey.TASKS$NO_GIT_PROVIDERS_DESCRIPTION)} - - - - - {t(I18nKey.TASKS$NO_GIT_PROVIDERS_CTA)} - - -
- ) : ( - - {t(I18nKey.TASKS$NO_TASKS_AVAILABLE)} - - ))} + {!hasSuggestedTasks && !isLoading && ( + + {t(I18nKey.TASKS$NO_TASKS_AVAILABLE)} + + )} {!isLoading && displayedTaskGroups && diff --git a/src/components/features/settings/git-settings/azure-devops-token-help-anchor.tsx b/src/components/features/settings/git-settings/azure-devops-token-help-anchor.tsx deleted file mode 100644 index edee1335ea..0000000000 --- a/src/components/features/settings/git-settings/azure-devops-token-help-anchor.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; - -export function AzureDevOpsTokenHelpAnchor() { - const { t } = useTranslation("openhands"); - - return ( -

- - {t(I18nKey.GIT$AZURE_DEVOPS_TOKEN_HELP)} - -

- ); -} diff --git a/src/components/features/settings/git-settings/azure-devops-token-input.tsx b/src/components/features/settings/git-settings/azure-devops-token-input.tsx deleted file mode 100644 index df451e7854..0000000000 --- a/src/components/features/settings/git-settings/azure-devops-token-input.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { SettingsInput } from "../settings-input"; -import { AzureDevOpsTokenHelpAnchor } from "./azure-devops-token-help-anchor"; -import { KeyStatusIcon } from "../key-status-icon"; - -interface AzureDevOpsTokenInputProps { - onChange: (value: string) => void; - onAzureDevOpsHostChange: (value: string) => void; - isAzureDevOpsTokenSet: boolean; - name: string; - azureDevOpsHostSet: string | null | undefined; -} - -export function AzureDevOpsTokenInput({ - onChange, - onAzureDevOpsHostChange, - isAzureDevOpsTokenSet, - name, - azureDevOpsHostSet, -}: AzureDevOpsTokenInputProps) { - const { t } = useTranslation("openhands"); - - return ( -
- " : ""} - startContent={ - isAzureDevOpsTokenSet && ( - - ) - } - /> - - {})} - name="azure-devops-host-input" - testId="azure-devops-host-input" - label={t(I18nKey.GIT$AZURE_DEVOPS_HOST)} - type="text" - className="w-full max-w-[680px]" - placeholder={t(I18nKey.GIT$AZURE_DEVOPS_HOST_PLACEHOLDER)} - defaultValue={azureDevOpsHostSet || undefined} - startContent={ - azureDevOpsHostSet && - azureDevOpsHostSet.trim() !== "" && ( - - ) - } - /> - - -
- ); -} diff --git a/src/components/features/settings/git-settings/bitbucket-dc-token-help-anchor.tsx b/src/components/features/settings/git-settings/bitbucket-dc-token-help-anchor.tsx deleted file mode 100644 index 46935f9d71..0000000000 --- a/src/components/features/settings/git-settings/bitbucket-dc-token-help-anchor.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Trans, useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; - -export function BitbucketDCTokenHelpAnchor() { - const { t } = useTranslation("openhands"); - - return ( -

- , - ]} - /> -

- ); -} diff --git a/src/components/features/settings/git-settings/bitbucket-dc-token-help-input.tsx b/src/components/features/settings/git-settings/bitbucket-dc-token-help-input.tsx deleted file mode 100644 index c0043bd494..0000000000 --- a/src/components/features/settings/git-settings/bitbucket-dc-token-help-input.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { SettingsInput } from "../settings-input"; -import { BitbucketDCTokenHelpAnchor } from "./bitbucket-dc-token-help-anchor"; -import { KeyStatusIcon } from "../key-status-icon"; -import { cn } from "#/utils/utils"; - -interface BitbucketDCTokenInputProps { - onChange: (value: string) => void; - onBitbucketDCHostChange: (value: string) => void; - isBitbucketDCTokenSet: boolean; - name: string; - bitbucketDCHostSet: string | null | undefined; - className?: string; -} - -export function BitbucketDCTokenInput({ - onChange, - onBitbucketDCHostChange, - isBitbucketDCTokenSet, - name, - bitbucketDCHostSet, - className, -}: BitbucketDCTokenInputProps) { - const { t } = useTranslation("openhands"); - - return ( -
- " : "username:token"} - startContent={ - isBitbucketDCTokenSet && ( - - ) - } - /> - - {})} - name="bitbucket-dc-host-input" - testId="bitbucket-dc-host-input" - label={t(I18nKey.BITBUCKET_DATA_CENTER$HOST_LABEL)} - type="text" - className="w-full max-w-[680px]" - placeholder="bitbucket.your-company.com" - defaultValue={bitbucketDCHostSet || undefined} - startContent={ - bitbucketDCHostSet && - bitbucketDCHostSet.trim() !== "" && ( - - ) - } - /> - - -
- ); -} diff --git a/src/components/features/settings/git-settings/bitbucket-token-help-anchor.tsx b/src/components/features/settings/git-settings/bitbucket-token-help-anchor.tsx deleted file mode 100644 index 9c70f3874d..0000000000 --- a/src/components/features/settings/git-settings/bitbucket-token-help-anchor.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Trans, useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; - -export function BitbucketTokenHelpAnchor() { - const { t } = useTranslation("openhands"); - - return ( -

- , - , - ]} - /> -

- ); -} diff --git a/src/components/features/settings/git-settings/bitbucket-token-input.tsx b/src/components/features/settings/git-settings/bitbucket-token-input.tsx deleted file mode 100644 index 121022fe3a..0000000000 --- a/src/components/features/settings/git-settings/bitbucket-token-input.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { SettingsInput } from "../settings-input"; -import { BitbucketTokenHelpAnchor } from "./bitbucket-token-help-anchor"; -import { KeyStatusIcon } from "../key-status-icon"; -import { cn } from "#/utils/utils"; - -interface BitbucketTokenInputProps { - onChange: (value: string) => void; - onBitbucketHostChange: (value: string) => void; - isBitbucketTokenSet: boolean; - name: string; - bitbucketHostSet: string | null | undefined; - className?: string; -} - -export function BitbucketTokenInput({ - onChange, - onBitbucketHostChange, - isBitbucketTokenSet, - name, - bitbucketHostSet, - className, -}: BitbucketTokenInputProps) { - const { t } = useTranslation("openhands"); - - return ( -
- " : "username:app_password"} - startContent={ - isBitbucketTokenSet && ( - - ) - } - /> - - {})} - name="bitbucket-host-input" - testId="bitbucket-host-input" - label={t(I18nKey.BITBUCKET$HOST_LABEL)} - type="text" - className="w-full max-w-[680px]" - placeholder="bitbucket.org" - defaultValue={bitbucketHostSet || undefined} - startContent={ - bitbucketHostSet && - bitbucketHostSet.trim() !== "" && ( - - ) - } - /> - - -
- ); -} diff --git a/src/components/features/settings/git-settings/configure-github-repositories-anchor.tsx b/src/components/features/settings/git-settings/configure-github-repositories-anchor.tsx deleted file mode 100644 index 14e0b383c2..0000000000 --- a/src/components/features/settings/git-settings/configure-github-repositories-anchor.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { BrandButton } from "../brand-button"; - -interface ConfigureGitHubRepositoriesAnchorProps { - slug: string; -} - -export function ConfigureGitHubRepositoriesAnchor({ - slug, -}: ConfigureGitHubRepositoriesAnchorProps) { - const { t } = useTranslation("openhands"); - - return ( -
- - window.open( - `https://github.com/apps/${slug}/installations/new`, - "_blank", - "noreferrer noopener", - ) - } - > - {t(I18nKey.GITHUB$CONFIGURE_REPOS)} - -
- ); -} diff --git a/src/components/features/settings/git-settings/forgejo-token-input.tsx b/src/components/features/settings/git-settings/forgejo-token-input.tsx deleted file mode 100644 index d9df2059dd..0000000000 --- a/src/components/features/settings/git-settings/forgejo-token-input.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { SettingsInput } from "../settings-input"; -import { KeyStatusIcon } from "../key-status-icon"; -import { cn } from "#/utils/utils"; - -interface ForgejoTokenInputProps { - onChange: (value: string) => void; - onForgejoHostChange: (value: string) => void; - isForgejoTokenSet: boolean; - name: string; - forgejoHostSet: string | null | undefined; - className?: string; -} - -export function ForgejoTokenInput({ - onChange, - onForgejoHostChange, - isForgejoTokenSet, - name, - forgejoHostSet, - className, -}: ForgejoTokenInputProps) { - const { t } = useTranslation("openhands"); - - return ( -
- " : ""} - startContent={ - isForgejoTokenSet && ( - - ) - } - /> - - {})} - name="forgejo-host-input" - testId="forgejo-host-input" - label={t(I18nKey.FORGEJO$HOST_LABEL)} - type="text" - className="w-full max-w-[680px]" - placeholder="codeberg.org" - defaultValue={forgejoHostSet || undefined} - startContent={ - forgejoHostSet && - forgejoHostSet.trim() !== "" && ( - - ) - } - /> -
- ); -} diff --git a/src/components/features/settings/git-settings/github-settings-inputs-skeleton.tsx b/src/components/features/settings/git-settings/github-settings-inputs-skeleton.tsx deleted file mode 100644 index 9885b98cec..0000000000 --- a/src/components/features/settings/git-settings/github-settings-inputs-skeleton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { InputSkeleton } from "../input-skeleton"; -import { SubtextSkeleton } from "../subtext-skeleton"; - -export function GitSettingInputsSkeleton() { - return ( -
-
- - -
- -
- - -
-
- ); -} diff --git a/src/components/features/settings/git-settings/github-token-help-anchor.tsx b/src/components/features/settings/git-settings/github-token-help-anchor.tsx deleted file mode 100644 index 3566cd32a0..0000000000 --- a/src/components/features/settings/git-settings/github-token-help-anchor.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Trans, useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; - -export function GitHubTokenHelpAnchor() { - const { t } = useTranslation("openhands"); - - return ( -

- , - , - ]} - /> -

- ); -} diff --git a/src/components/features/settings/git-settings/github-token-input.tsx b/src/components/features/settings/git-settings/github-token-input.tsx deleted file mode 100644 index d6d0fa3b1b..0000000000 --- a/src/components/features/settings/git-settings/github-token-input.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { SettingsInput } from "../settings-input"; -import { GitHubTokenHelpAnchor } from "./github-token-help-anchor"; -import { KeyStatusIcon } from "../key-status-icon"; -import { cn } from "#/utils/utils"; - -interface GitHubTokenInputProps { - onChange: (value: string) => void; - onGitHubHostChange: (value: string) => void; - isGitHubTokenSet: boolean; - name: string; - githubHostSet: string | null | undefined; - className?: string; -} - -export function GitHubTokenInput({ - onChange, - onGitHubHostChange, - isGitHubTokenSet, - name, - githubHostSet, - className, -}: GitHubTokenInputProps) { - const { t } = useTranslation("openhands"); - - return ( -
- " : ""} - startContent={ - isGitHubTokenSet && ( - - ) - } - /> - - {})} - name="github-host-input" - testId="github-host-input" - label={t(I18nKey.GITHUB$HOST_LABEL)} - type="text" - className="w-full max-w-[680px]" - placeholder="github.com" - defaultValue={githubHostSet || undefined} - startContent={ - githubHostSet && - githubHostSet.trim() !== "" && ( - - ) - } - /> - - -
- ); -} diff --git a/src/components/features/settings/git-settings/gitlab-token-help-anchor.tsx b/src/components/features/settings/git-settings/gitlab-token-help-anchor.tsx deleted file mode 100644 index a871d8ee30..0000000000 --- a/src/components/features/settings/git-settings/gitlab-token-help-anchor.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Trans, useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; - -export function GitLabTokenHelpAnchor() { - const { t } = useTranslation("openhands"); - - return ( -

- , - , - ]} - /> -

- ); -} diff --git a/src/components/features/settings/git-settings/gitlab-token-input.tsx b/src/components/features/settings/git-settings/gitlab-token-input.tsx deleted file mode 100644 index cd9ce6c256..0000000000 --- a/src/components/features/settings/git-settings/gitlab-token-input.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { SettingsInput } from "../settings-input"; -import { GitLabTokenHelpAnchor } from "./gitlab-token-help-anchor"; -import { KeyStatusIcon } from "../key-status-icon"; -import { cn } from "#/utils/utils"; - -interface GitLabTokenInputProps { - onChange: (value: string) => void; - onGitLabHostChange: (value: string) => void; - isGitLabTokenSet: boolean; - name: string; - gitlabHostSet: string | null | undefined; - className?: string; -} - -export function GitLabTokenInput({ - onChange, - onGitLabHostChange, - isGitLabTokenSet, - name, - gitlabHostSet, - className, -}: GitLabTokenInputProps) { - const { t } = useTranslation("openhands"); - - return ( -
- " : ""} - startContent={ - isGitLabTokenSet && ( - - ) - } - /> - - {})} - name="gitlab-host-input" - testId="gitlab-host-input" - label={t(I18nKey.GITLAB$HOST_LABEL)} - type="text" - className="w-full max-w-[680px]" - placeholder="gitlab.com" - defaultValue={gitlabHostSet || undefined} - startContent={ - gitlabHostSet && - gitlabHostSet.trim() !== "" && ( - - ) - } - /> - - -
- ); -} diff --git a/src/components/features/settings/git-settings/install-slack-app-anchor.tsx b/src/components/features/settings/git-settings/install-slack-app-anchor.tsx deleted file mode 100644 index 2edc452b9b..0000000000 --- a/src/components/features/settings/git-settings/install-slack-app-anchor.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { BrandButton } from "../brand-button"; - -export function InstallSlackAppAnchor() { - const { t } = useTranslation("openhands"); - - return ( -
- - window.open( - "https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,channels:history,chat:write,groups:history,im:history,mpim:history,users:read&user_scope=", - "_blank", - "noreferrer noopener", - ) - } - > - {t(I18nKey.SLACK$INSTALL_APP)} - -
- ); -} diff --git a/src/components/features/settings/git-settings/webhook-status-badge.tsx b/src/components/features/settings/git-settings/webhook-status-badge.tsx deleted file mode 100644 index 159a0c6275..0000000000 --- a/src/components/features/settings/git-settings/webhook-status-badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { Typography } from "#/ui/typography"; - -export interface WebhookStatusBadgeProps { - webhookInstalled: boolean; - installationResult?: { success: boolean; error: string | null } | null; -} - -export function WebhookStatusBadge({ - webhookInstalled, - installationResult, -}: WebhookStatusBadgeProps) { - const { t } = useTranslation("openhands"); - - if (installationResult) { - if (installationResult.success) { - return ( - - {t(I18nKey.GITLAB$WEBHOOK_STATUS_INSTALLED)} - - ); - } - return ( - - - {t(I18nKey.GITLAB$WEBHOOK_STATUS_FAILED)} - - - ); - } - - if (webhookInstalled) { - return ( - - {t(I18nKey.GITLAB$WEBHOOK_STATUS_INSTALLED)} - - ); - } - - return ( - - {t(I18nKey.GITLAB$WEBHOOK_STATUS_NOT_INSTALLED)} - - ); -} diff --git a/src/components/features/settings/project-management/configure-modal.tsx b/src/components/features/settings/project-management/configure-modal.tsx deleted file mode 100644 index f4a99ad39b..0000000000 --- a/src/components/features/settings/project-management/configure-modal.tsx +++ /dev/null @@ -1,456 +0,0 @@ -import React, { useState } from "react"; -import { Trans, useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { BrandButton } from "#/components/features/settings/brand-button"; -import { SettingsInput } from "#/components/features/settings/settings-input"; -import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; -import { ModalBody } from "#/components/shared/modals/modal-body"; -import { - BaseModalDescription, - BaseModalTitle, -} from "#/components/shared/modals/confirmation-modals/base-modal"; -import { SettingsSwitch } from "#/components/features/settings/settings-switch"; -import { useValidateIntegration } from "#/hooks/mutation/use-validate-integration"; - -interface ConfigureButtonProps { - onClick: () => void; - isDisabled: boolean; - text?: string; - "data-testid"?: string; -} - -export function ConfigureButton({ - onClick, - isDisabled, - text, - "data-testid": dataTestId, -}: ConfigureButtonProps) { - const { t } = useTranslation("openhands"); - return ( - - {text || t(I18nKey.PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL)} - - ); -} - -interface ConfigureModalProps { - isOpen: boolean; - onClose: () => void; - onConfirm: (data: { - workspace: string; - webhookSecret: string; - serviceAccountEmail: string; - serviceAccountApiKey: string; - isActive: boolean; - }) => void; - onLink: (workspace: string) => void; - onUnlink?: () => void; - platformName: string; - platform: "jira" | "jira-dc" | "linear"; - integrationData?: { - id: number; - keycloak_user_id: string; - status: string; - workspace?: { - id: number; - name: string; - status: string; - editable: boolean; - }; - } | null; -} - -export function ConfigureModal({ - isOpen, - onClose, - onConfirm, - onLink, - onUnlink, - platformName, - platform, - integrationData, -}: ConfigureModalProps) { - const { t } = useTranslation("openhands"); - const [workspace, setWorkspace] = useState(""); - const [webhookSecret, setWebhookSecret] = useState(""); - const [serviceAccountEmail, setServiceAccountEmail] = useState(""); - const [serviceAccountApiKey, setServiceAccountApiKey] = useState(""); - const [isActive, setIsActive] = useState(true); - const [showConfigurationFields, setShowConfigurationFields] = useState(false); - - // Determine initial state based on integrationData - const existingWorkspace = integrationData?.workspace; - const isWorkspaceEditable = existingWorkspace?.editable ?? false; - - // Set initial workspace value when modal opens - React.useEffect(() => { - if (isOpen && existingWorkspace) { - setWorkspace(existingWorkspace.name); - setShowConfigurationFields(isWorkspaceEditable); - } else if (isOpen && !existingWorkspace) { - setWorkspace(""); - setShowConfigurationFields(false); - } - }, [isOpen, existingWorkspace, isWorkspaceEditable]); - - // Helper function to get platform-specific placeholder - const getWorkspacePlaceholder = () => { - if (platform === "jira") { - return I18nKey.PROJECT_MANAGEMENT$JIRA_WORKSPACE_NAME_PLACEHOLDER; - } - if (platform === "jira-dc") { - return I18nKey.PROJECT_MANAGEMENT$JIRA_DC_WORKSPACE_NAME_PLACEHOLDER; - } - return I18nKey.PROJECT_MANAGEMENT$LINEAR_WORKSPACE_NAME_PLACEHOLDER; - }; - - // Validation states - const [workspaceError, setWorkspaceError] = useState(null); - const [webhookSecretError, setWebhookSecretError] = useState( - null, - ); - const [emailError, setEmailError] = useState(null); - const [apiKeyError, setApiKeyError] = useState(null); - - const validateMutation = useValidateIntegration(platform, { - onSuccess: (data) => { - if (data.data.status === "active") { - // Validation successful, proceed with linking - onLink(workspace.trim()); - } else { - // Show configuration fields for further setup - setShowConfigurationFields(true); - setIsActive(true); - } - }, - onError: (error) => { - if (error.response?.status === 404) { - // Integration not found, show configuration fields - setShowConfigurationFields(true); - setIsActive(true); - } else { - // Other errors - still show configuration fields as fallback - setShowConfigurationFields(true); - setIsActive(true); - } - }, - }); - - // Validation functions - const validateWorkspace = (value: string) => { - const isValid = /^[a-zA-Z0-9\-_.]*$/.test(value); - if (!isValid && value.length > 0) { - setWorkspaceError( - t(I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR), - ); - } else { - setWorkspaceError(null); - } - return isValid; - }; - - const validateWebhookSecret = (value: string) => { - const hasSpaces = /\s/.test(value); - if (hasSpaces) { - setWebhookSecretError( - t(I18nKey.PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR), - ); - } else { - setWebhookSecretError(null); - } - return !hasSpaces; - }; - - const validateEmail = (value: string) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const isValid = emailRegex.test(value) || value.length === 0; - if (!isValid && value.length > 0) { - setEmailError( - t(I18nKey.PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR), - ); - } else { - setEmailError(null); - } - return isValid; - }; - - const validateApiKey = (value: string) => { - const hasSpaces = /\s/.test(value); - if (hasSpaces) { - setApiKeyError( - t(I18nKey.PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR), - ); - } else { - setApiKeyError(null); - } - return !hasSpaces; - }; - - // Input handlers with validation - const handleWorkspaceChange = (value: string) => { - setWorkspace(value); - validateWorkspace(value); - }; - - const handleWebhookSecretChange = (value: string) => { - setWebhookSecret(value); - validateWebhookSecret(value); - }; - - const handleEmailChange = (value: string) => { - setServiceAccountEmail(value); - validateEmail(value); - }; - - const handleApiKeyChange = (value: string) => { - setServiceAccountApiKey(value); - validateApiKey(value); - }; - - const handleClose = () => { - setWorkspace(""); - setWebhookSecret(""); - setServiceAccountEmail(""); - setServiceAccountApiKey(""); - setIsActive(false); - setShowConfigurationFields(false); - setWorkspaceError(null); - setWebhookSecretError(null); - setEmailError(null); - setApiKeyError(null); - onClose(); - }; - - if (!isOpen) { - return null; - } - - const handleConnect = () => { - if (showConfigurationFields) { - // Full configuration flow (either new configuration or editing existing) - onConfirm({ - workspace, - webhookSecret, - serviceAccountEmail, - serviceAccountApiKey, - isActive, - }); - } else if (!existingWorkspace) { - // First check the workspace with validation for new integrations - validateMutation.mutate(workspace.trim()); - } - // For existing workspace that's not editable, no action needed - // This case shouldn't happen as the button should be hidden - }; - - const isConnectDisabled = showConfigurationFields - ? !workspace.trim() || - !webhookSecret.trim() || - !serviceAccountEmail.trim() || - !serviceAccountApiKey.trim() || - workspaceError !== null || - webhookSecretError !== null || - emailError !== null || - apiKeyError !== null || - validateMutation.isPending - : !workspace.trim() || - workspaceError !== null || - validateMutation.isPending; - - return ( - - - - - {showConfigurationFields ? ( - , - a: ( -
- Check the document for more information - - ), - }} - /> - ) : ( - , - a: ( - - Check the document for more information - - ), - }} - /> - )} -

- {t(I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_HINT, { - platform: platformName, - })} -

- -
-
-
-
- -
- {existingWorkspace && onUnlink && ( - - {t(I18nKey.PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL)} - - )} -
- {workspaceError && ( -

{workspaceError}

- )} -
- - {showConfigurationFields && ( - <> -
- - {webhookSecretError && ( -

- {webhookSecretError} -

- )} -
-
- - {emailError && ( -

{emailError}

- )} -
-
- - {apiKeyError && ( -

{apiKeyError}

- )} -
-
- - {t(I18nKey.PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL)} - -
- - )} -
-
- {/* Hide the connect/edit button if workspace exists but is not editable */} - {(!existingWorkspace || isWorkspaceEditable) && ( - - {(() => { - if (existingWorkspace && showConfigurationFields) { - return t(I18nKey.PROJECT_MANAGEMENT$UPDATE_BUTTON_LABEL); - } - return t(I18nKey.PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL); - })()} - - )} - - {t(I18nKey.FEEDBACK$CANCEL_LABEL)} - -
- - - ); -} diff --git a/src/components/features/settings/project-management/integration-row.tsx b/src/components/features/settings/project-management/integration-row.tsx deleted file mode 100644 index 5060a9d004..0000000000 --- a/src/components/features/settings/project-management/integration-row.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { useIntegrationStatus } from "#/hooks/query/use-integration-status"; -import { useLinkIntegration } from "#/hooks/mutation/use-link-integration"; -import { useUnlinkIntegration } from "#/hooks/mutation/use-unlink-integration"; -import { useConfigureIntegration } from "#/hooks/mutation/use-configure-integration"; -import { I18nKey } from "#/i18n/declaration"; -import { - ConfigureButton, - ConfigureModal, -} from "#/components/features/settings/project-management/configure-modal"; - -interface IntegrationRowProps { - platform: "jira" | "jira-dc" | "linear"; - platformName: string; - "data-testid"?: string; -} - -export function IntegrationRow({ - platform, - platformName, - "data-testid": dataTestId, -}: IntegrationRowProps) { - const [isConfigureModalOpen, setConfigureModalOpen] = React.useState(false); - const { t } = useTranslation("openhands"); - - const { data: integrationData, isLoading: isStatusLoading } = - useIntegrationStatus(platform); - - const linkMutation = useLinkIntegration(platform, { - onSettled: () => { - setConfigureModalOpen(false); - }, - }); - - const unlinkMutation = useUnlinkIntegration(platform, { - onSettled: () => { - setConfigureModalOpen(false); - }, - }); - - const configureMutation = useConfigureIntegration(platform, { - onSettled: () => { - setConfigureModalOpen(false); - }, - }); - - const handleConfigure = () => { - setConfigureModalOpen(true); - }; - - const handleLink = (workspace: string) => { - linkMutation.mutate(workspace); - }; - - const handleUnlink = () => { - unlinkMutation.mutate(); - }; - - const handleConfigureConfirm = (data: { - workspace: string; - webhookSecret: string; - serviceAccountEmail: string; - serviceAccountApiKey: string; - isActive: boolean; - }) => { - configureMutation.mutate(data); - }; - - const isLoading = - isStatusLoading || - linkMutation.isPending || - unlinkMutation.isPending || - configureMutation.isPending; - - // Determine if integration is active and workspace exists - const isIntegrationActive = integrationData?.status === "active"; - const hasWorkspace = integrationData?.workspace; - - // Determine button text based on integration state - const buttonText = - isIntegrationActive && hasWorkspace - ? t(I18nKey.PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL) - : t(I18nKey.PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL); - - return ( -
- {platformName} -
- -
- setConfigureModalOpen(false)} - onConfirm={handleConfigureConfirm} - onLink={handleLink} - onUnlink={handleUnlink} - platformName={platformName} - platform={platform} - integrationData={integrationData} - /> -
- ); -} diff --git a/src/components/features/settings/project-management/project-management-integration.tsx b/src/components/features/settings/project-management/project-management-integration.tsx deleted file mode 100644 index d05bdbf1ea..0000000000 --- a/src/components/features/settings/project-management/project-management-integration.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { IntegrationRow } from "./integration-row"; -import { useConfig } from "#/hooks/query/use-config"; - -export function ProjectManagementIntegration() { - const { t } = useTranslation("openhands"); - const { data: config } = useConfig(); - - return ( -
-

- {t(I18nKey.PROJECT_MANAGEMENT$TITLE)} -

-
- {config?.feature_flags?.enable_jira && ( - - )} - {config?.feature_flags?.enable_jira_dc && ( - - )} - {config?.feature_flags?.enable_linear && ( - - )} -
-
- ); -} diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 28a4b82828..8af30f5de5 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -1,6 +1,5 @@ export * from "../features/settings"; export { AppSettingsScreen as AppSettings } from "../../routes/app-settings"; -export { GitSettingsScreen as GitSettings } from "../../routes/git-settings"; export { LlmSettingsScreen as LLMSettings } from "../../routes/llm-settings"; export { MCPSettingsScreen as MCPSettings } from "../../routes/mcp-settings"; export { SecretsSettingsScreen as SecretsSettings } from "../../routes/secrets-settings"; diff --git a/src/constants/settings-nav.tsx b/src/constants/settings-nav.tsx index 6047b42ac6..2a8d749b8e 100644 --- a/src/constants/settings-nav.tsx +++ b/src/constants/settings-nav.tsx @@ -5,7 +5,6 @@ import MemoryIcon from "#/icons/memory_icon.svg?react"; import ServerProcessIcon from "#/icons/server-process.svg?react"; import SettingsGearIcon from "#/icons/settings-gear.svg?react"; import CircuitIcon from "#/icons/u-circuit.svg?react"; -import PuzzlePieceIcon from "#/icons/u-puzzle-piece.svg?react"; export interface SettingsNavItem { icon: React.ReactElement; @@ -39,11 +38,6 @@ export const OSS_NAV_ITEMS: SettingsNavItem[] = [ to: "/settings/skills", text: "SETTINGS$NAV_SKILLS", }, - { - icon: , - to: "/settings/integrations", - text: "SETTINGS$NAV_INTEGRATIONS", - }, { icon: , to: "/settings/app", diff --git a/src/hooks/mutation/use-add-git-providers.ts b/src/hooks/mutation/use-add-git-providers.ts deleted file mode 100644 index d4ce49d348..0000000000 --- a/src/hooks/mutation/use-add-git-providers.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { SecretsService } from "#/api/secrets-service"; -import { Provider, ProviderToken } from "#/types/settings"; -import { useTracking } from "#/hooks/use-tracking"; -import { SETTINGS_QUERY_KEYS } from "#/hooks/query/query-keys"; - -export const useAddGitProviders = () => { - const queryClient = useQueryClient(); - const { trackGitProviderConnected } = useTracking(); - - return useMutation({ - mutationFn: ({ - providers, - }: { - providers: Partial>; - }) => SecretsService.addGitProvider(providers), - onSuccess: async (_, { providers }) => { - const connectedProviders = Object.entries(providers) - .filter(([, value]) => value?.token && value.token.trim() !== "") - .map(([key]) => key); - - if (connectedProviders.length > 0) { - trackGitProviderConnected({ - providers: connectedProviders, - }); - } - - await queryClient.invalidateQueries({ - queryKey: SETTINGS_QUERY_KEYS.personal(), - }); - }, - meta: { - disableToast: true, - }, - }); -}; diff --git a/src/hooks/mutation/use-configure-integration.ts b/src/hooks/mutation/use-configure-integration.ts deleted file mode 100644 index 8f628bac27..0000000000 --- a/src/hooks/mutation/use-configure-integration.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useTranslation } from "react-i18next"; - -import { openHands } from "#/api/open-hands-axios"; -import { I18nKey } from "#/i18n/declaration"; -import { displayErrorToast } from "#/utils/custom-toast-handlers"; -import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; - -interface ConfigureIntegrationData { - workspace: string; - webhookSecret: string; - serviceAccountEmail: string; - serviceAccountApiKey: string; - isActive: boolean; -} - -export function useConfigureIntegration( - platform: "jira" | "jira-dc" | "linear", - { - onSettled, - }: { - onSettled: () => void; - }, -) { - const queryClient = useQueryClient(); - const { t } = useTranslation("openhands"); - - return useMutation({ - mutationFn: async (data: ConfigureIntegrationData) => { - const input = { - workspace_name: data.workspace, - webhook_secret: data.webhookSecret, - svc_acc_email: data.serviceAccountEmail, - svc_acc_api_key: data.serviceAccountApiKey, - is_active: data.isActive, - }; - - const response = await openHands.post( - `/integration/${platform}/workspaces`, - input, - ); - - const { success, redirect, authorizationUrl } = response.data; - - if (success) { - if (redirect) { - if (authorizationUrl) { - window.location.href = authorizationUrl; - } else { - throw new Error("Could not get authorization URL from the server."); - } - } else { - window.location.reload(); - } - } else { - throw new Error("Configuration failed"); - } - - return response.data; - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["integration-status", platform], - }); - }, - onError: (error) => { - const errorMessage = retrieveAxiosErrorMessage(error); - displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC)); - }, - onSettled, - }); -} diff --git a/src/hooks/mutation/use-delete-git-providers.ts b/src/hooks/mutation/use-delete-git-providers.ts deleted file mode 100644 index b646dd9ab2..0000000000 --- a/src/hooks/mutation/use-delete-git-providers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { SecretsService } from "#/api/secrets-service"; -import { SETTINGS_QUERY_KEYS } from "#/hooks/query/query-keys"; - -export const useDeleteGitProviders = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: () => SecretsService.deleteGitProviders(), - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: SETTINGS_QUERY_KEYS.personal(), - }); - }, - meta: { - disableToast: true, - }, - }); -}; diff --git a/src/hooks/mutation/use-link-integration.ts b/src/hooks/mutation/use-link-integration.ts deleted file mode 100644 index c9098c92d0..0000000000 --- a/src/hooks/mutation/use-link-integration.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useTranslation } from "react-i18next"; - -import { openHands } from "#/api/open-hands-axios"; -import { I18nKey } from "#/i18n/declaration"; -import { displayErrorToast } from "#/utils/custom-toast-handlers"; -import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; - -export function useLinkIntegration( - platform: "jira" | "jira-dc" | "linear", - { - onSettled, - }: { - onSettled: () => void; - }, -) { - const queryClient = useQueryClient(); - const { t } = useTranslation("openhands"); - - return useMutation({ - mutationFn: async (workspace: string) => { - const input = { - workspace_name: workspace, - }; - - const response = await openHands.post( - `/integration/${platform}/workspaces/link`, - input, - ); - - const { success, redirect, authorizationUrl } = response.data; - - if (success) { - if (redirect) { - if (authorizationUrl) { - window.location.href = authorizationUrl; - } else { - throw new Error("Could not get authorization URL from the server."); - } - } else { - window.location.reload(); - } - } else { - throw new Error("Link integration failed"); - } - - return response.data; - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["integration-status", platform], - }); - }, - onError: (error) => { - const errorMessage = retrieveAxiosErrorMessage(error); - displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC)); - }, - onSettled, - }); -} diff --git a/src/hooks/mutation/use-unlink-integration.ts b/src/hooks/mutation/use-unlink-integration.ts deleted file mode 100644 index c2dc2bb99b..0000000000 --- a/src/hooks/mutation/use-unlink-integration.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useTranslation } from "react-i18next"; - -import { openHands } from "#/api/open-hands-axios"; -import { I18nKey } from "#/i18n/declaration"; -import { - displayErrorToast, - displaySuccessToast, -} from "#/utils/custom-toast-handlers"; -import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; - -export function useUnlinkIntegration( - platform: "jira" | "jira-dc" | "linear", - { - onSettled, - }: { - onSettled: () => void; - }, -) { - const queryClient = useQueryClient(); - const { t } = useTranslation("openhands"); - - return useMutation({ - mutationFn: () => - openHands.post(`/integration/${platform}/workspaces/unlink`), - onSuccess: () => { - displaySuccessToast(t(I18nKey.SETTINGS$SAVED)); - queryClient.invalidateQueries({ - queryKey: ["integration-status", platform], - }); - }, - onError: (error) => { - const errorMessage = retrieveAxiosErrorMessage(error); - displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC)); - }, - onSettled, - }); -} diff --git a/src/hooks/mutation/use-validate-integration.ts b/src/hooks/mutation/use-validate-integration.ts deleted file mode 100644 index f8dc3c2544..0000000000 --- a/src/hooks/mutation/use-validate-integration.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { useMutation } from "@tanstack/react-query"; -import axios from "axios"; -import { useTranslation } from "react-i18next"; - -import { openHands } from "#/api/open-hands-axios"; -import { I18nKey } from "#/i18n/declaration"; -import { displayErrorToast } from "#/utils/custom-toast-handlers"; -import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; - -export function useValidateIntegration( - platform: "jira" | "jira-dc" | "linear", - { - onSuccess, - onError, - }: { - onSuccess: (data: any) => void; - onError: (error: any) => void; - }, -) { - const { t } = useTranslation("openhands"); - - return useMutation({ - mutationFn: (workspace?: string) => { - const workspaceParam = workspace ? `/${workspace}` : ""; - return openHands.get( - `/integration/${platform}/workspaces/validate${workspaceParam}`, - ); - }, - onSuccess, - onError: (error) => { - if (axios.isAxiosError(error) && error.response?.status === 404) { - onError(error); - } else { - const errorMessage = retrieveAxiosErrorMessage(error); - displayErrorToast( - errorMessage || - t(I18nKey.PROJECT_MANAGEMENT$VALIDATE_INTEGRATION_ERROR), - ); - } - }, - }); -} diff --git a/src/hooks/query/use-integration-status.ts b/src/hooks/query/use-integration-status.ts deleted file mode 100644 index d54e674bf3..0000000000 --- a/src/hooks/query/use-integration-status.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { openHands } from "#/api/open-hands-axios"; - -export function useIntegrationStatus(platform: "jira" | "jira-dc" | "linear") { - return useQuery({ - queryKey: ["integration-status", platform], - queryFn: async () => { - try { - const response = await openHands.get( - `/integration/${platform}/workspaces/link`, - ); - return response.data; - } catch (error) { - if (axios.isAxiosError(error) && error.response?.status === 404) { - return null; - } - throw error; - } - }, - }); -} diff --git a/src/hooks/use-settings-nav-items.ts b/src/hooks/use-settings-nav-items.ts index 791f12f586..69bf42c3de 100644 --- a/src/hooks/use-settings-nav-items.ts +++ b/src/hooks/use-settings-nav-items.ts @@ -1,10 +1,6 @@ import { useConfig } from "#/hooks/query/use-config"; -import { useActiveBackend } from "#/contexts/active-backend-context"; import { OSS_NAV_ITEMS, SettingsNavItem } from "#/constants/settings-nav"; -import { - isLocalOnlySettingsPath, - isSettingsPageHidden, -} from "#/utils/settings-utils"; +import { isSettingsPageHidden } from "#/utils/settings-utils"; import { I18nKey } from "#/i18n/declaration"; export type SettingsNavRenderedItem = @@ -15,12 +11,8 @@ export type SettingsNavRenderedItem = export function useSettingsNavItems(): SettingsNavRenderedItem[] { const { data: config } = useConfig(); const featureFlags = config?.feature_flags; - const active = useActiveBackend(); - const isCloud = active.backend.kind === "cloud"; return OSS_NAV_ITEMS.filter( - (item) => - !isSettingsPageHidden(item.to, featureFlags) && - !(isCloud && isLocalOnlySettingsPath(item.to)), + (item) => !isSettingsPageHidden(item.to, featureFlags), ).map((item) => ({ type: "item", item })); } diff --git a/src/i18n/translation.json b/src/i18n/translation.json index e30583492e..933e0621e6 100644 --- a/src/i18n/translation.json +++ b/src/i18n/translation.json @@ -1478,23 +1478,6 @@ "uk": "Невірний JSON", "ca": "JSON no vàlid" }, - "HOME$CONNECT_PROVIDER_MESSAGE": { - "en": "To get started with suggested tasks, please connect your GitHub, GitLab, Bitbucket, or Azure DevOps account.", - "ja": "提案されたタスクを始めるには、GitHub、GitLab、Bitbucket、またはAzure DevOpsアカウントを接続してください。", - "zh-CN": "要开始使用建议的任务,请连接您的GitHub、GitLab、Bitbucket或Azure DevOps账户。", - "zh-TW": "要開始使用建議的任務,請連接您的GitHub、GitLab、Bitbucket或Azure DevOps帳戶。", - "ko-KR": "제안된 작업을 시작하려면 GitHub, GitLab, Bitbucket 또는 Azure DevOps 계정을 연결하세요.", - "no": "For å komme i gang med foreslåtte oppgaver, vennligst koble til GitHub, GitLab, Bitbucket eller Azure DevOps-kontoen din.", - "it": "Per iniziare con le attività suggerite, collega il tuo account GitHub, GitLab, Bitbucket o Azure DevOps.", - "pt": "Para começar com tarefas sugeridas, conecte sua conta GitHub, GitLab, Bitbucket ou Azure DevOps.", - "es": "Para comenzar con las tareas sugeridas, conecte su cuenta de GitHub, GitLab, Bitbucket o Azure DevOps.", - "ar": "للبدء بالمهام المقترحة، يرجى ربط حساب GitHub أو GitLab أو Bitbucket أو Azure DevOps الخاص بك.", - "fr": "Pour commencer avec les tâches suggérées, veuillez connecter votre compte GitHub, GitLab, Bitbucket ou Azure DevOps.", - "tr": "Önerilen görevlerle başlamak için lütfen GitHub, GitLab, Bitbucket veya Azure DevOps hesabınızı bağlayın.", - "de": "Um mit vorgeschlagenen Aufgaben zu beginnen, verbinden Sie bitte Ihr GitHub-, GitLab-, Bitbucket- oder Azure DevOps-Konto.", - "uk": "Щоб розпочати роботу з запропонованими завданнями, підключіть свій обліковий запис GitHub, GitLab, Bitbucket або Azure DevOps.", - "ca": "Per començar amb les tasques suggerides, connecteu el vostre compte de GitHub, GitLab, Bitbucket o Azure DevOps." - }, "HOME$LETS_START_BUILDING": { "en": "Let's Start Building!", "ja": "開発を始めましょう!", @@ -4742,23 +4725,6 @@ "uk": "Аналізатор безпеки", "ca": "Analitzador de seguretat" }, - "SETTINGS$NAV_INTEGRATIONS": { - "en": "Integrations", - "ja": "統合", - "zh-CN": "集成", - "zh-TW": "整合", - "ko-KR": "통합", - "no": "Integrasjoner", - "it": "Integrazioni", - "pt": "Integrações", - "es": "Integraciones", - "ar": "التكامل", - "fr": "Intégrations", - "tr": "Entegrasyonlar", - "de": "Integrationen", - "uk": "Інтеграції", - "ca": "Integracions" - }, "SETTINGS$NAV_APPLICATION": { "en": "Application", "ja": "アプリケーション", @@ -16795,57 +16761,6 @@ "uk": "Вибрати репозиторій", "ca": "Selecciona un repositori" }, - "TASKS$NO_GIT_PROVIDERS_TITLE": { - "en": "Connect a Git provider", - "ja": "Gitプロバイダーを接続", - "zh-CN": "连接 Git 提供商", - "zh-TW": "連接 Git 提供商", - "ko-KR": "Git 제공자 연결", - "no": "Koble til en Git-leverandør", - "ar": "قم بتوصيل موفر Git", - "de": "Git-Anbieter verbinden", - "fr": "Connecter un fournisseur Git", - "it": "Collega un provider Git", - "pt": "Conectar um provedor Git", - "es": "Conectar un proveedor Git", - "tr": "Git sağlayıcısını bağla", - "uk": "Підключити постачальник Git", - "ca": "Connecta un proveïdor de Git" - }, - "TASKS$NO_GIT_PROVIDERS_DESCRIPTION": { - "en": "Connect a Git provider to see suggested tasks from your repositories.", - "ja": "Gitプロバイダーを接続して、リポジトリからの提案タスクを表示します。", - "zh-CN": "连接 Git 提供商以查看来自存储库的建议任务。", - "zh-TW": "連接 Git 提供商以查看來自存儲庫的建議任務。", - "ko-KR": "Git 제공자를 연결하여 저장소의 추천 작업을 확인하세요.", - "no": "Koble til en Git-leverandør for å se foreslåtte oppgaver fra dine repositorier.", - "ar": "قم بتوصيل موفر Git لرؤية المهام المقترحة من مستودعاتك.", - "de": "Verbinde einen Git-Anbieter, um vorgeschlagene Aufgaben aus deinen Repositories zu sehen.", - "fr": "Connectez un fournisseur Git pour voir les tâches suggérées depuis vos dépôts.", - "it": "Collega un provider Git per vedere le attività suggerite dai tuoi repository.", - "pt": "Conecte um provedor Git para ver tarefas sugeridas dos seus repositórios.", - "es": "Conecta un proveedor Git para ver tareas sugeridas de tus repositorios.", - "tr": "Depolarınızdan önerilen görevleri görmek için bir Git sağlayıcısı bağlayın.", - "uk": "Підключіть постачальник Git, щоб бачити запропоновані завдання з ваших репозиторіїв.", - "ca": "Connecteu un proveïdor de Git per veure les tasques suggerides dels vostres repositoris." - }, - "TASKS$NO_GIT_PROVIDERS_CTA": { - "en": "Go to Integrations", - "ja": "統合へ移動", - "zh-CN": "前往集成", - "zh-TW": "前往整合", - "ko-KR": "통합으로 이동", - "no": "Gå til integrasjoner", - "ar": "الانتقال إلى التكاملات", - "de": "Zu Integrationen", - "fr": "Aller aux intégrations", - "it": "Vai alle integrazioni", - "pt": "Ir para integrações", - "es": "Ir a integraciones", - "tr": "Entegrasyonlara git", - "uk": "Перейти до інтеграцій", - "ca": "Vés a Integracions" - }, "TASKS$SUGGESTED_TASKS": { "en": "Suggested Tasks", "ja": "推奨タスク", @@ -17135,23 +17050,6 @@ "uk": "Максимальний бюджет на розмову в доларах США", "ca": "Pressupost màxim per conversa en USD" }, - "GIT$DISCONNECT_TOKENS": { - "en": "Disconnect Tokens", - "ja": "トークンを切断", - "zh-CN": "断开令牌连接", - "zh-TW": "中斷令牌連接", - "ko-KR": "토큰 연결 해제", - "no": "Koble fra tokens", - "it": "Disconnetti token", - "pt": "Desconectar tokens", - "es": "Desconectar tokens", - "ar": "قطع اتصال الرموز", - "fr": "Déconnecter les jetons", - "tr": "Token bağlantısını kes", - "de": "Token trennen", - "uk": "Відключити токени", - "ca": "Desconnecta els tokens" - }, "API$TAVILY_KEY_EXAMPLE": { "en": "tvly-dev-...", "ja": "tvly-dev-...", @@ -18002,482 +17900,6 @@ "uk": "Електронна пошта для git-комітів", "ca": "Correu electrònic per als commits de Git" }, - "PROJECT_MANAGEMENT$TITLE": { - "en": "Project Management", - "ja": "プロジェクト管理", - "zh-CN": "项目管理", - "zh-TW": "專案管理", - "ko-KR": "프로젝트 관리", - "no": "Prosjektledelse", - "it": "Gestione del progetto", - "pt": "Gerenciamento de projetos", - "es": "Gestión de proyectos", - "ar": "إدارة المشاريع", - "fr": "Gestion de projet", - "tr": "Proje Yönetimi", - "de": "Projektmanagement", - "uk": "Управління проектами", - "ca": "Gestió de projectes" - }, - "PROJECT_MANAGEMENT$VALIDATE_INTEGRATION_ERROR": { - "en": "Failed to validate integration. Please try again later.", - "ja": "統合の検証に失敗しました。しばらくしてからもう一度お試しください.", - "zh-CN": "无法验证集成。请稍后重试.", - "zh-TW": "無法驗證整合。請稍後重試.", - "ko-KR": "통합을 검증하지 못했습니다. 나중에 다시 시도해 주세요.", - "no": "Kunne ikke validere integrasjonen. Prøv på nytt senere.", - "it": "Impossibile convalidare l'integrazione. Riprova più tardi.", - "pt": "Falha ao validar a integração. Tente novamente mais tarde.", - "es": "No se pudo validar la integración. Inténtelo de nuevo más tarde.", - "ar": "فشل التحقق من التكامل. يُرجى المحاولة لاحقًا.", - "fr": "Échec de la validation de l'intégration. Veuillez réessayer ultérieurement.", - "tr": "Entegrasyon doğrulanamadı. Lütfen daha sonra tekrar deneyin.", - "de": "Die Integration konnte nicht validiert werden. Bitte versuchen Sie es später noch einmal.", - "uk": "Не вдалося перевірити інтеграцію. Спробуйте пізніше.", - "ca": "No s'ha pogut validar la integració. Torneu-ho a intentar més tard." - }, - "PROJECT_MANAGEMENT$LINK_BUTTON_LABEL": { - "en": "Link", - "ja": "リンク", - "zh-CN": "关联", - "zh-TW": "關聯", - "ko-KR": "링크", - "no": "Link", - "it": "Collegamento", - "pt": "Link", - "es": "Enlace", - "ar": "وصلة", - "fr": "Lien", - "tr": "Bağlantı", - "de": "Link", - "uk": "Посилання", - "ca": "Vincula" - }, - "PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL": { - "en": "Unlink", - "ja": "リンクを解除する", - "zh-CN": "取消链接", - "zh-TW": "取消連結", - "ko-KR": "풀리다", - "no": "Fjern tilknytningen", - "it": "Scollega", - "pt": "Desvincular", - "es": "Desconectar", - "ar": "إلغاء الارتباط", - "fr": "Dissocier", - "tr": "Bağlantıyı kaldır", - "de": "Verknüpfung aufheben", - "uk": "Від’єднати", - "ca": "Desvincula" - }, - "PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE": { - "en": "Link Workspace", - "ja": "ワークスペースをリンク", - "zh-CN": "链接工作区", - "zh-TW": "連結工作區", - "ko-KR": "작업공간 링크", - "no": "Link arbeidsområde", - "it": "Collega area di lavoro", - "pt": "Vincular espaço de trabalho", - "es": "Vincular espacio de trabajo", - "ar": "ربط مساحة العمل", - "fr": "Lier l'espace de travail", - "tr": "Çalışma Alanını Bağla", - "de": "Arbeitsbereich verknüpfen", - "uk": "Пов'язати робочу область", - "ca": "Vincula l'espai de treball" - }, - "PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL": { - "en": "Configure", - "ja": "設定する", - "zh-CN": "配置", - "zh-TW": "配置", - "ko-KR": "구성", - "no": "Konfigurer", - "it": "Configura", - "pt": "Configurar", - "es": "Configurar", - "ar": "تكوين", - "fr": "Configurer", - "tr": "Yapılandır", - "de": "Konfigurieren", - "uk": "Налаштувати", - "ca": "Configura" - }, - "PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL": { - "en": "Edit", - "ja": "編集", - "zh-CN": "编辑", - "zh-TW": "編輯", - "ko-KR": "편집", - "no": "Rediger", - "it": "Modifica", - "pt": "Editar", - "es": "Editar", - "ar": "تحرير", - "fr": "Modifier", - "tr": "Düzenle", - "de": "Bearbeiten", - "uk": "Редагувати", - "ca": "Edita" - }, - "PROJECT_MANAGEMENT$UPDATE_BUTTON_LABEL": { - "en": "Update", - "ja": "更新", - "zh-CN": "更新", - "zh-TW": "更新", - "ko-KR": "업데이트", - "no": "Oppdater", - "it": "Aggiorna", - "pt": "Atualizar", - "es": "Actualizar", - "ar": "تحديث", - "fr": "Mettre à jour", - "tr": "Güncelle", - "de": "Aktualisieren", - "uk": "Оновити", - "ca": "Actualitza" - }, - "PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE": { - "en": "Configure {{platform}} Integration", - "ja": "{{platform}}統合を設定", - "zh-CN": "配置{{platform}}集成", - "zh-TW": "配置{{platform}}集成", - "ko-KR": "{{platform}} 통합 구성", - "no": "Konfigurer {{platform}}-integrasjon", - "it": "Configura integrazione {{platform}}", - "pt": "Configurar integração {{platform}}", - "es": "Configurar integración de {{platform}}", - "ar": "تكوين تكامل {{platform}}", - "fr": "Configurer l'intégration {{platform}}", - "tr": "{{platform}} Entegrasyonunu Yapılandır", - "de": "{{platform}}-Integration konfigurieren", - "uk": "Налаштувати інтеграцію {{platform}}", - "ca": "Configura la integració de {{platform}}" - }, - "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_1": { - "en": "Important: Make sure the workspace integration for your target workspace is already configured. Check the documentation for more information.", - "ja": "重要: 対象のワークスペースのワークスペース統合がすでに設定されていることを確認してください。詳しくはドキュメントをご覧ください。", - "zh-CN": "重要提示:请确保目标工作区的工作区集成已配置完毕。查看文档了解更多信息。", - "zh-TW": "重要提示:請確保目標工作區的工作區整合已設定完成。查看文件以了解更多資訊。", - "ko-KR": "중요:대상 작업 공간에 대한 작업 공간 통합이 이미 구성되어 있는지 확인하세요. 자세한 내용은 설명서를 참조하세요.", - "no": "Viktig:Sørg for at arbeidsområdeintegrasjonen for målarbeidsområdet ditt allerede er konfigurert. Se dokumentasjonen for mer informasjon.", - "it": "Importante: Assicurati che l'integrazione dell'area di lavoro per l'area di lavoro di destinazione sia già configurata. Consulta la documentazione per ulteriori informazioni.", - "pt": "Importante:Certifique-se de que a integração do workspace de destino já esteja configurada. Consulte a documentação para obter mais informações.", - "es": "Importante: Asegúrate de que la integración del espacio de trabajo de destino ya esté configurada. Consulta la documentación para obtener más información.", - "ar": "هام: تأكد من إعداد تكامل مساحة العمل لمساحة العمل المستهدفة. راجع المستند لمزيد من المعلومات.", - "fr": "Important :Assurez-vous que l'intégration de l'espace de travail cible est déjà configurée. Consultez la documentation pour plus d'informations.", - "tr": "Önemli: Hedef çalışma alanınız için çalışma alanı entegrasyonunun zaten yapılandırılmış olduğundan emin olun. Daha fazla bilgi için belgelere bakın.", - "de": "Wichtig:Stellen Sie sicher, dass die Arbeitsbereichsintegration für Ihren Zielarbeitsbereich bereits konfiguriert ist. Weitere Informationen finden Sie in der Dokumentation.", - "uk": "Важливо: Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте документацію для отримання додаткової інформації.", - "ca": "Important: Assegureu-vos que la integració de l'espai de treball per al vostre espai de treball de destinació ja estigui configurada. Consulteu la documentació per obtenir més informació." - }, - "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_2": { - "en": "Important: Check the documentation for more information about configuring the workspace integration or updating an existing integration.", - "ja": "重要: ワークスペース統合の設定や既存の統合の更新について詳しくはドキュメントをご確認ください。", - "zh-CN": "重要提示:有关配置工作区集成或更新现有集成的更多信息,请查看文档。", - "zh-TW": "重要提示:有關配置工作區整合或更新現有整合的更多資訊,請查看文件。", - "ko-KR": "중요: 작업공간 통합 구성이나 기존 통합 업데이트에 대한 자세한 내용은 설명서를 확인하세요.", - "no": "Viktig: Se dokumentasjonen for mer informasjon om konfigurering av arbeidsområdeintegrasjon eller oppdatering av eksisterende integrasjon.", - "it": "Importante: Consulta la documentazione per ulteriori informazioni sulla configurazione dell'integrazione dell'area di lavoro o sull'aggiornamento di un'integrazione esistente.", - "pt": "Importante: Consulte a documentação para obter mais informações sobre como configurar a integração do workspace ou atualizar uma integração existente.", - "es": "Importante: Consulte la documentación para obtener más información sobre la configuración de la integración del espacio de trabajo o la actualización de una integración existente.", - "ar": "هام: راجع الوثائق لمزيد من المعلومات حول تكوين تكامل مساحة العمل أو تحديث تكامل موجود.", - "fr": "Important : Consultez la documentation pour plus d'informations sur la configuration de l'intégration de l'espace de travail ou la mise à jour d'une intégration existante.", - "tr": "Önemli: Çalışma alanı entegrasyonunu yapılandırma veya mevcut bir entegrasyonu güncelleme hakkında daha fazla bilgi için belgelere bakın.", - "de": "Wichtig: Weitere Informationen zur Konfiguration der Arbeitsbereichsintegration oder zur Aktualisierung einer bestehenden Integration finden Sie in der Dokumentation.", - "uk": "Важливо: Перегляньте документацію для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції.", - "ca": "Important: Consulteu la documentació per obtenir més informació sobre la configuració de la integració de l'espai de treball o l'actualització d'una integració existent." - }, - "PROJECT_MANAGEMENT$WORKSPACE_NAME_HINT": { - "en": "Workspace name can be found in the browser URL when you're accessing a resource (eg: issue) in {{platform}}.", - "ja": "ワークスペース名は、{{platform}}のリソース(例:イシュー)にアクセスする際のブラウザURLで確認できます。", - "zh-CN": "工作区名称可以在访问{{platform}}资源(如:问题)时的浏览器URL中找到。", - "zh-TW": "工作區名稱可以在存取{{platform}}資源(如:問題)時的瀏覽器URL中找到。", - "ko-KR": "작업공간 이름은 {{platform}}의 리소스(예: 이슈)에 액세스할 때 브라우저 URL에서 찾을 수 있습니다.", - "no": "Arbeidsområdenavn kan finnes i nettleser-URL-en når du får tilgang til en ressurs (f.eks: sak) i {{platform}}.", - "it": "Il nome dell'area di lavoro può essere trovato nell'URL del browser quando stai accedendo a una risorsa (es: issue) in {{platform}}.", - "pt": "O nome do workspace pode ser encontrado na URL do navegador quando você está acessando um recurso (ex: issue) em {{platform}}.", - "es": "El nombre del espacio de trabajo se puede encontrar en la URL del navegador cuando accedes a un recurso (ej: issue) en {{platform}}.", - "ar": "يمكن العثور على اسم مساحة العمل في عنوان URL للمتصفح عند الوصول إلى مورد (مثل: مشكلة) في {{platform}}.", - "fr": "Le nom de l'espace de travail peut être trouvé dans l'URL du navigateur lorsque vous accédez à une ressource (ex : issue) dans {{platform}}.", - "tr": "Çalışma alanı adı, {{platform}}'da bir kaynağa (örn: sorun) erişirken tarayıcı URL'sinde bulunabilir.", - "de": "Der Arbeitsbereichsname ist in der Browser-URL zu finden, wenn Sie auf eine Ressource (z.B.: Issue) in {{platform}} zugreifen.", - "uk": "Назву робочого простору можна знайти в URL браузера під час доступу до ресурсу (наприклад: проблема) в {{platform}}.", - "ca": "El nom de l'espai de treball es pot trobar a la URL del navegador quan accediu a un recurs (p. ex.: problema) a {{platform}}." - }, - "PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL": { - "en": "Workspace Name", - "ja": "ワークスペース名", - "zh-CN": "工作区名称", - "zh-TW": "工作區名稱", - "ko-KR": "작업공간 이름", - "no": "Navn på arbeidsområde", - "it": "Nome dell'area di lavoro", - "pt": "Nome do espaço de trabalho", - "es": "Nombre del espacio de trabajo", - "ar": "اسم مساحة العمل", - "fr": "Nom de l'espace de travail", - "tr": "Çalışma Alanı Adı", - "de": "Arbeitsbereichsname", - "uk": "Назва робочої області", - "ca": "Nom de l'espai de treball" - }, - "PROJECT_MANAGEMENT$JIRA_WORKSPACE_NAME_PLACEHOLDER": { - "en": "yourcompany.atlassian.net", - "ja": "yourcompany.atlassian.net", - "zh-CN": "yourcompany.atlassian.net", - "zh-TW": "yourcompany.atlassian.net", - "ko-KR": "yourcompany.atlassian.net", - "no": "yourcompany.atlassian.net", - "it": "yourcompany.atlassian.net", - "pt": "yourcompany.atlassian.net", - "es": "yourcompany.atlassian.net", - "ar": "yourcompany.atlassian.net", - "fr": "yourcompany.atlassian.net", - "tr": "yourcompany.atlassian.net", - "de": "yourcompany.atlassian.net", - "uk": "yourcompany.atlassian.net", - "ca": "yourcompany.atlassian.net" - }, - "PROJECT_MANAGEMENT$JIRA_DC_WORKSPACE_NAME_PLACEHOLDER": { - "en": "jira.yourcompany.com", - "ja": "jira.yourcompany.com", - "zh-CN": "jira.yourcompany.com", - "zh-TW": "jira.yourcompany.com", - "ko-KR": "jira.yourcompany.com", - "no": "jira.yourcompany.com", - "it": "jira.yourcompany.com", - "pt": "jira.yourcompany.com", - "es": "jira.yourcompany.com", - "ar": "jira.yourcompany.com", - "fr": "jira.yourcompany.com", - "tr": "jira.yourcompany.com", - "de": "jira.yourcompany.com", - "uk": "jira.yourcompany.com", - "ca": "jira.yourcompany.com" - }, - "PROJECT_MANAGEMENT$LINEAR_WORKSPACE_NAME_PLACEHOLDER": { - "en": "yourcompany", - "ja": "yourcompany", - "zh-CN": "yourcompany", - "zh-TW": "yourcompany", - "ko-KR": "yourcompany", - "no": "yourcompany", - "it": "yourcompany", - "pt": "yourcompany", - "es": "yourcompany", - "ar": "yourcompany", - "fr": "yourcompany", - "tr": "yourcompany", - "de": "yourcompany", - "uk": "yourcompany", - "ca": "yourcompany" - }, - "PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL": { - "en": "Webhook Secret", - "ja": "Webhook シークレット", - "zh-CN": "Webhook 秘密", - "zh-TW": "Webhook 秘密", - "ko-KR": "웹훅 비밀", - "no": "Webhook Secret", - "it": "Segreto del webhook", - "pt": "Segredo do webhook", - "es": "Secreto del webhook", - "ar": "سر الويب هوك", - "fr": "Secret du webhook", - "tr": "Web Kancası Sırrı", - "de": "Webhook-Geheimnis", - "uk": "Секрет вебхуку", - "ca": "Secret del webhook" - }, - "PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER": { - "en": "whsec_abcd1234efgh5678ijkl", - "ja": "whsec_abcd1234efgh5678ijkl", - "zh-CN": "whsec_abcd1234efgh5678ijkl", - "zh-TW": "whsec_abcd1234efgh5678ijkl", - "ko-KR": "whsec_abcd1234efgh5678ijkl", - "no": "whsec_abcd1234efgh5678ijkl", - "it": "whsec_abcd1234efgh5678ijkl", - "pt": "whsec_abcd1234efgh5678ijkl", - "es": "whsec_abcd1234efgh5678ijkl", - "ar": "whsec_abcd1234efgh5678ijkl", - "fr": "whsec_abcd1234efgh5678ijkl", - "tr": "whsec_abcd1234efgh5678ijkl", - "de": "whsec_abcd1234efgh5678ijkl", - "uk": "whsec_abcd1234efgh5678ijkl", - "ca": "whsec_abcd1234efgh5678ijkl" - }, - "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL": { - "en": "Service Account Email", - "ja": "サービスアカウントのメールアドレス", - "zh-CN": "服务帐户电子邮件", - "zh-TW": "服務帳號電子郵件", - "ko-KR": "서비스 계정 이메일", - "no": "Tjenestekonto e-post", - "it": "E-mail dell'account di servizio", - "pt": "E-mail da conta de serviço", - "es": "Correo electrónico de cuenta de servicio", - "ar": "البريد الإلكتروني لحساب الخدمة", - "fr": "E-mail du compte de service", - "tr": "Hizmet Hesabı E-postası", - "de": "E-Mail-Adresse des Dienstkontos", - "uk": "Електронна адреса облікового запису служби", - "ca": "Correu electrònic del compte de servei" - }, - "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_PLACEHOLDER": { - "en": "test@example.com", - "ja": "test@example.com", - "zh-CN": "test@example.com", - "zh-TW": "test@example.com", - "ko-KR": "test@example.com", - "no": "test@example.com", - "it": "test@example.com", - "pt": "test@example.com", - "es": "test@example.com", - "ar": "test@example.com", - "fr": "test@example.com", - "tr": "test@example.com", - "de": "test@example.com", - "uk": "test@example.com", - "ca": "test@example.com" - }, - "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_LABEL": { - "en": "Service Account API Key", - "ja": "サービスアカウントAPIキー", - "zh-CN": "服务帐户 API 密钥", - "zh-TW": "服務帳戶 API 金鑰", - "ko-KR": "서비스 계정 API 키", - "no": "Tjenestekonto API-nøkkel", - "it": "Chiave API dell'account di servizio", - "pt": "Chave de API da conta de serviço", - "es": "Clave API de cuenta de servicio", - "ar": "مفتاح API لحساب الخدمة", - "fr": "Clé API du compte de service", - "tr": "Hizmet Hesabı API Anahtarı", - "de": "API-Schlüssel des Dienstkontos", - "uk": "Ключ API облікового запису служби", - "ca": "Clau d'API del compte de servei" - }, - "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER": { - "en": "sk-1234567890abcdef...", - "ja": "sk-1234567890abcdef...", - "zh-CN": "sk-1234567890abcdef...", - "zh-TW": "sk-1234567890abcdef...", - "ko-KR": "sk-1234567890abcdef...", - "no": "sk-1234567890abcdef...", - "it": "sk-1234567890abcdef...", - "pt": "sk-1234567890abcdef...", - "es": "sk-1234567890abcdef...", - "ar": "sk-1234567890abcdef...", - "fr": "sk-1234567890abcdef...", - "tr": "sk-1234567890abcdef...", - "de": "sk-1234567890abcdef...", - "uk": "sk-1234567890abcdef...", - "ca": "sk-1234567890abcdef..." - }, - "PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL": { - "en": "Connect", - "ja": "接続する", - "zh-CN": "连接", - "zh-TW": "連接", - "ko-KR": "연결하다", - "no": "Koble til", - "it": "Collegare", - "pt": "Conectar", - "es": "Conectar", - "ar": "يتصل", - "fr": "Connecter", - "tr": "Bağlamak", - "de": "Verbinden", - "uk": "Підключитися", - "ca": "Connecta" - }, - "PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL": { - "en": "Active", - "ja": "アクティブ", - "zh-CN": "积极的", - "zh-TW": "積極的", - "ko-KR": "활동적인", - "no": "Aktiv", - "it": "Attiva", - "pt": "Ativa", - "es": "Activa", - "ar": "نشيط", - "fr": "Actif", - "tr": "Aktif", - "de": "Aktiv", - "uk": "Активний", - "ca": "Actiu" - }, - "PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR": { - "en": "Workspace name can only contain letters, numbers, hyphens, and underscores", - "ja": "ワークスペース名は文字、数字、ハイフン、アンダースコアのみ使用できます", - "zh-CN": "工作区名称只能包含字母、数字、连字符和下划线", - "zh-TW": "工作區名稱只能包含字母、數字、連字符和底線", - "ko-KR": "작업공간 이름은 문자, 숫자, 하이픈, 밑줄만 포함할 수 있습니다", - "no": "Arbeidsområdenavn kan bare inneholde bokstaver, tall, bindestrek og understrek", - "it": "Il nome dell'area di lavoro può contenere solo lettere, numeri, trattini e trattini bassi", - "pt": "O nome do workspace pode conter apenas letras, números, hífens e sublinhados", - "es": "El nombre del espacio de trabajo solo puede contener letras, números, guiones y guiones bajos", - "ar": "يمكن أن يحتوي اسم مساحة العمل على أحرف وأرقام وشرطات وشرطات سفلية فقط", - "fr": "Le nom de l'espace de travail ne peut contenir que des lettres, des chiffres, des tirets et des traits de soulignement", - "tr": "Çalışma alanı adı yalnızca harfler, sayılar, tire ve alt çizgi içerebilir", - "de": "Der Arbeitsbereichsname darf nur Buchstaben, Zahlen, Bindestriche und Unterstriche enthalten", - "uk": "Назва робочого простору може містити тільки літери, цифри, дефіси та підкреслення", - "ca": "El nom de l'espai de treball només pot contenir lletres, números, guions i guions baixos" - }, - "PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR": { - "en": "Webhook secret cannot contain spaces", - "ja": "Webhookシークレットにはスペースを含めることはできません", - "zh-CN": "Webhook密钥不能包含空格", - "zh-TW": "Webhook密鑰不能包含空格", - "ko-KR": "웹훅 시크릿에는 공백을 포함할 수 없습니다", - "no": "Webhook-hemmelighet kan ikke inneholde mellomrom", - "it": "Il segreto del webhook non può contenere spazi", - "pt": "O segredo do webhook não pode conter espaços", - "es": "El secreto del webhook no puede contener espacios", - "ar": "لا يمكن أن يحتوي سر الويب هوك على مسافات", - "fr": "Le secret du webhook ne peut pas contenir d'espaces", - "tr": "Webhook sırrı boşluk içeremez", - "de": "Das Webhook-Geheimnis darf keine Leerzeichen enthalten", - "uk": "Секрет веб-хука не може містити пробіли", - "ca": "El secret del webhook no pot contenir espais" - }, - "PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR": { - "en": "Please enter a valid email address", - "ja": "有効なメールアドレスを入力してください", - "zh-CN": "请输入有效的电子邮件地址", - "zh-TW": "請輸入有效的電子郵件地址", - "ko-KR": "유효한 이메일 주소를 입력하세요", - "no": "Vennligst skriv inn en gyldig e-postadresse", - "it": "Inserisci un indirizzo email valido", - "pt": "Por favor, insira um endereço de email válido", - "es": "Por favor, introduce una dirección de correo electrónico válida", - "ar": "يرجى إدخال عنوان بريد إلكتروني صحيح", - "fr": "Veuillez saisir une adresse e-mail valide", - "tr": "Lütfen geçerli bir e-posta adresi girin", - "de": "Bitte geben Sie eine gültige E-Mail-Adresse ein", - "uk": "Будь ласка, введіть дійсну адресу електронної пошти", - "ca": "Introduïu una adreça de correu electrònic vàlida" - }, - "PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR": { - "en": "API key cannot contain spaces", - "ja": "APIキーにはスペースを含めることはできません", - "zh-CN": "API密钥不能包含空格", - "zh-TW": "API密鑰不能包含空格", - "ko-KR": "API 키에는 공백을 포함할 수 없습니다", - "no": "API-nøkkel kan ikke inneholde mellomrom", - "it": "La chiave API non può contenere spazi", - "pt": "A chave da API não pode conter espaços", - "es": "La clave API no puede contener espacios", - "ar": "لا يمكن أن يحتوي مفتاح API على مسافات", - "fr": "La clé API ne peut pas contenir d'espaces", - "tr": "API anahtarı boşluk içeremez", - "de": "Der API-Schlüssel darf keine Leerzeichen enthalten", - "uk": "Ключ API не може містити пробіли", - "ca": "La clau d'API no pot contenir espais" - }, "SETTINGS$MCP_SERVER_TYPE_SSE": { "en": "SSE", "ja": "SSE", @@ -18971,57 +18393,6 @@ "uk": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}", "ca": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}" }, - "PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER": { - "en": "myworkspace", - "ja": "私のワークスペース", - "zh-CN": "我的工作空间", - "zh-TW": "我的工作區", - "ko-KR": "내워크스페이스", - "no": "mittarbeidsområde", - "it": "mioworkspace", - "pt": "meuworkspace", - "es": "miespaciodetrabajo", - "ar": "مساحةعملي", - "fr": "monworkspace", - "tr": "benimworkspace", - "de": "meinarbeitsbereich", - "uk": "моя-робоча-область", - "ca": "myworkspace" - }, - "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION": { - "en": "Important: Check the documentation for more information about configuring the workspace integration or updating an existing integration.", - "ja": "重要: ワークスペース統合の設定や既存の統合の更新について詳しくはドキュメントをご確認ください。", - "zh-CN": "重要提示:有关配置工作区集成或更新现有集成的更多信息,请查看文档。", - "zh-TW": "重要提示:有關配置工作區整合或更新現有整合的更多資訊,請查看文件。", - "ko-KR": "중요: 작업공간 통합 구성이나 기존 통합 업데이트에 대한 자세한 내용은 설명서를 확인하세요.", - "no": "Viktig: Se dokumentasjonen for mer informasjon om konfigurering av arbeidsområdeintegrasjon eller oppdatering av eksisterende integrasjon.", - "it": "Importante: Consulta la documentazione per ulteriori informazioni sulla configurazione dell'integrazione dell'area di lavoro o sull'aggiornamento di un'integrazione esistente.", - "pt": "Importante: Consulte a documentação para obter mais informações sobre como configurar a integração do workspace ou atualizar uma integração existente.", - "es": "Importante: Consulte la documentación para obtener más información sobre la configuración de la integración del espacio de trabajo o la actualización de una integración existente.", - "ar": "هام: راجع الوثائق لمزيد من المعلومات حول تكوين تكامل مساحة العمل أو تحديث تكامل موجود.", - "fr": "Important : Consultez la documentation pour plus d'informations sur la configuration de l'intégration de l'espace de travail ou la mise à jour d'une intégration existante.", - "tr": "Önemli: Çalışma alanı entegrasyonunu yapılandırma veya mevcut bir entegrasyonu güncelleme hakkında daha fazla bilgi için belgelere bakın.", - "de": "Wichtig: Weitere Informationen zur Konfiguration der Arbeitsbereichsintegration oder zur Aktualisierung einer bestehenden Integration finden Sie in der Dokumentation.", - "uk": "Важливо: Перегляньте документацію для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції.", - "ca": "Important: Consulteu la documentació per obtenir més informació sobre la configuració de la integració de l'espai de treball o l'actualització d'una integració existent." - }, - "PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION": { - "en": "Important: Make sure the workspace integration for your target workspace is already configured. Check the documentation for more information.", - "ja": "重要: 対象のワークスペースのワークスペース統合がすでに設定されていることを確認してください。詳しくはドキュメントをご覧ください。", - "zh-CN": "重要提示:请确保目标工作区的工作区集成已配置完毕。查看文档了解更多信息。", - "zh-TW": "重要提示:請確保目標工作區的工作區整合已設定完成。查看文件以了解更多資訊。", - "ko-KR": "중요:대상 작업 공간에 대한 작업 공간 통합이 이미 구성되어 있는지 확인하세요. 자세한 내용은 설명서를 참조하세요.", - "no": "Viktig:Sørg for at arbeidsområdeintegrasjonen for målarbeidsområdet ditt allerede er konfigurert. Se dokumentasjonen for mer informasjon.", - "it": "Importante: Assicurati che l'integrazione dell'area di lavoro per l'area di lavoro di destinazione sia già configurata. Consulta la documentazione per ulteriori informazioni.", - "pt": "Importante:Certifique-se de que a integração do workspace de destino já esteja configurada. Consulte a documentação para obter mais informações.", - "es": "Importante: Asegúrate de que la integración del espacio de trabajo de destino ya esté configurada. Consulta la documentación para obtener más información.", - "ar": "هام: تأكد من إعداد تكامل مساحة العمل لمساحة العمل المستهدفة. راجع المستند لمزيد من المعلومات.", - "fr": "Important :Assurez-vous que l'intégration de l'espace de travail cible est déjà configurée. Consultez la documentation pour plus d'informations.", - "tr": "Önemli: Hedef çalışma alanınız için çalışma alanı entegrasyonunun zaten yapılandırılmış olduğundan emin olun. Daha fazla bilgi için belgelere bakın.", - "de": "Wichtig:Stellen Sie sicher, dass die Arbeitsbereichsintegration für Ihren Zielarbeitsbereich bereits konfiguriert ist. Weitere Informationen finden Sie in der Dokumentation.", - "uk": "Важливо: Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте документацію для отримання додаткової інформації.", - "ca": "Important: Assegureu-vos que la integració de l'espai de treball per al vostre espai de treball de destinació ja estigui configurada. Consulteu la documentació per obtenir més informació." - }, "SETTINGS": { "en": "Environment variables must follow KEY=value format", "ja": "A server with this URL already exists for the selected type", diff --git a/src/mocks/settings-handlers.ts b/src/mocks/settings-handlers.ts index 401e3487e2..5d8123481d 100644 --- a/src/mocks/settings-handlers.ts +++ b/src/mocks/settings-handlers.ts @@ -44,11 +44,7 @@ export const createMockWebClientConfig = ( posthog_client_key: "test-posthog-key", feature_flags: { hide_llm_settings: false, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, hide_users_page: false, - hide_integrations_page: false, ...overrides.feature_flags, }, providers_configured: [], @@ -452,11 +448,7 @@ export const SETTINGS_HANDLERS = [ posthog_client_key: "fake-posthog-client-key", feature_flags: { hide_llm_settings: false, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, hide_users_page: false, - hide_integrations_page: false, }, providers_configured: [], maintenance_start_time: null, diff --git a/src/routes.ts b/src/routes.ts index 3d731ae72d..4befc539e5 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -15,7 +15,6 @@ export default [ route("verification", "routes/verification-settings.tsx"), route("mcp", "routes/mcp-settings.tsx"), route("skills", "routes/skills-settings.tsx"), - route("integrations", "routes/git-settings.tsx"), route("app", "routes/app-settings.tsx"), route("secrets", "routes/secrets-settings.tsx"), ]), diff --git a/src/routes/git-settings.tsx b/src/routes/git-settings.tsx deleted file mode 100644 index cfa7874f4d..0000000000 --- a/src/routes/git-settings.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { useConfig } from "#/hooks/query/use-config"; -import { useSettings } from "#/hooks/query/use-settings"; -import { BrandButton } from "#/components/features/settings/brand-button"; -import { GitHubTokenInput } from "#/components/features/settings/git-settings/github-token-input"; -import { GitLabTokenInput } from "#/components/features/settings/git-settings/gitlab-token-input"; -import { BitbucketTokenInput } from "#/components/features/settings/git-settings/bitbucket-token-input"; -import { BitbucketDCTokenInput } from "#/components/features/settings/git-settings/bitbucket-dc-token-help-input"; -import { AzureDevOpsTokenInput } from "#/components/features/settings/git-settings/azure-devops-token-input"; -import { ForgejoTokenInput } from "#/components/features/settings/git-settings/forgejo-token-input"; -import { I18nKey } from "#/i18n/declaration"; -import { - displayErrorToast, - displaySuccessToast, -} from "#/utils/custom-toast-handlers"; -import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; -import { GitSettingInputsSkeleton } from "#/components/features/settings/git-settings/github-settings-inputs-skeleton"; -import { useAddGitProviders } from "#/hooks/mutation/use-add-git-providers"; -import { useDeleteGitProviders } from "#/hooks/mutation/use-delete-git-providers"; -import { useUserProviders } from "#/hooks/use-user-providers"; -import { ProjectManagementIntegration } from "#/components/features/settings/project-management/project-management-integration"; -import { Provider, ProviderToken } from "#/types/settings"; - -export function GitSettingsScreen() { - const { t } = useTranslation("openhands"); - - const { mutate: saveGitProviders, isPending } = useAddGitProviders(); - const { mutate: disconnectGitTokens, isPending: isDisconnecting } = - useDeleteGitProviders(); - - const { data: settings, isLoading } = useSettings(); - const { providers } = useUserProviders(); - const { data: config } = useConfig(); - - const [githubTokenInputHasValue, setGithubTokenInputHasValue] = - React.useState(false); - const [gitlabTokenInputHasValue, setGitlabTokenInputHasValue] = - React.useState(false); - const [bitbucketTokenInputHasValue, setBitbucketTokenInputHasValue] = - React.useState(false); - const [bitbucketDCTokenInputHasValue, setBitbucketDCTokenInputHasValue] = - React.useState(false); - const [azureDevOpsTokenInputHasValue, setAzureDevOpsTokenInputHasValue] = - React.useState(false); - const [forgejoTokenInputHasValue, setForgejoTokenInputHasValue] = - React.useState(false); - - const [githubHostInputHasValue, setGithubHostInputHasValue] = - React.useState(false); - const [gitlabHostInputHasValue, setGitlabHostInputHasValue] = - React.useState(false); - const [bitbucketHostInputHasValue, setBitbucketHostInputHasValue] = - React.useState(false); - const [bitbucketDCHostInputHasValue, setBitbucketDCHostInputHasValue] = - React.useState(false); - const [azureDevOpsHostInputHasValue, setAzureDevOpsHostInputHasValue] = - React.useState(false); - const [forgejoHostInputHasValue, setForgejoHostInputHasValue] = - React.useState(false); - - const existingGithubHost = settings?.provider_tokens_set.github; - const existingGitlabHost = settings?.provider_tokens_set.gitlab; - const existingBitbucketHost = settings?.provider_tokens_set.bitbucket; - const existingBitbucketDCHost = - settings?.provider_tokens_set.bitbucket_data_center; - const existingAzureDevOpsHost = settings?.provider_tokens_set.azure_devops; - const existingForgejoHost = settings?.provider_tokens_set.forgejo; - - const isGitHubTokenSet = providers.includes("github"); - const isGitLabTokenSet = providers.includes("gitlab"); - const isBitbucketTokenSet = providers.includes("bitbucket"); - const isBitbucketDCTokenSet = providers.includes("bitbucket_data_center"); - const isAzureDevOpsTokenSet = providers.includes("azure_devops"); - const isForgejoTokenSet = providers.includes("forgejo"); - - const formAction = async (formData: FormData) => { - const disconnectButtonClicked = - formData.get("disconnect-tokens-button") !== null; - - if (disconnectButtonClicked) { - disconnectGitTokens(undefined, { - onSuccess: () => { - displaySuccessToast(t(I18nKey.SETTINGS$SAVED)); - }, - onError: (error) => { - const errorMessage = retrieveAxiosErrorMessage(error); - displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC)); - }, - }); - return; - } - - const githubToken = ( - formData.get("github-token-input")?.toString() || "" - ).trim(); - const gitlabToken = ( - formData.get("gitlab-token-input")?.toString() || "" - ).trim(); - const bitbucketToken = ( - formData.get("bitbucket-token-input")?.toString() || "" - ).trim(); - const bitbucketDCToken = ( - formData.get("bitbucket-dc-token-input")?.toString() || "" - ).trim(); - const azureDevOpsToken = ( - formData.get("azure-devops-token-input")?.toString() || "" - ).trim(); - const forgejoToken = ( - formData.get("forgejo-token-input")?.toString() || "" - ).trim(); - const githubHost = ( - formData.get("github-host-input")?.toString() || "" - ).trim(); - const gitlabHost = ( - formData.get("gitlab-host-input")?.toString() || "" - ).trim(); - const bitbucketHost = ( - formData.get("bitbucket-host-input")?.toString() || "" - ).trim(); - const bitbucketDCHost = ( - formData.get("bitbucket-dc-host-input")?.toString() || "" - ).trim(); - const azureDevOpsHost = ( - formData.get("azure-devops-host-input")?.toString() || "" - ).trim(); - const forgejoHost = ( - formData.get("forgejo-host-input")?.toString() || "" - ).trim(); - - const providerTokens: Partial> = { - github: { token: githubToken, host: githubHost }, - gitlab: { token: gitlabToken, host: gitlabHost }, - bitbucket: { token: bitbucketToken, host: bitbucketHost }, - bitbucket_data_center: { token: bitbucketDCToken, host: bitbucketDCHost }, - azure_devops: { token: azureDevOpsToken, host: azureDevOpsHost }, - forgejo: { token: forgejoToken, host: forgejoHost }, - }; - - saveGitProviders( - { - providers: providerTokens, - }, - { - onSuccess: () => { - displaySuccessToast(t(I18nKey.SETTINGS$SAVED)); - }, - onError: (error) => { - const errorMessage = retrieveAxiosErrorMessage(error); - displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC)); - }, - onSettled: () => { - setGithubTokenInputHasValue(false); - setGitlabTokenInputHasValue(false); - setBitbucketTokenInputHasValue(false); - setBitbucketDCTokenInputHasValue(false); - setAzureDevOpsTokenInputHasValue(false); - setForgejoTokenInputHasValue(false); - setGithubHostInputHasValue(false); - setGitlabHostInputHasValue(false); - setBitbucketHostInputHasValue(false); - setBitbucketDCHostInputHasValue(false); - setAzureDevOpsHostInputHasValue(false); - setForgejoHostInputHasValue(false); - }, - }, - ); - }; - - const formIsClean = - !githubTokenInputHasValue && - !gitlabTokenInputHasValue && - !bitbucketTokenInputHasValue && - !bitbucketDCTokenInputHasValue && - !azureDevOpsTokenInputHasValue && - !forgejoTokenInputHasValue && - !githubHostInputHasValue && - !gitlabHostInputHasValue && - !bitbucketHostInputHasValue && - !bitbucketDCHostInputHasValue && - !azureDevOpsHostInputHasValue && - !forgejoHostInputHasValue; - - const shouldRenderProjectManagementIntegrations = - config?.feature_flags?.enable_jira || - config?.feature_flags?.enable_jira_dc || - config?.feature_flags?.enable_linear; - - return ( -
- {!isLoading && ( -
- {shouldRenderProjectManagementIntegrations && ( -
- -
- )} - - { - setGithubTokenInputHasValue(!!value); - }} - onGitHubHostChange={(value) => { - setGithubHostInputHasValue(!!value); - }} - githubHostSet={existingGithubHost} - /> - - { - setGitlabTokenInputHasValue(!!value); - }} - onGitLabHostChange={(value) => { - setGitlabHostInputHasValue(!!value); - }} - gitlabHostSet={existingGitlabHost} - /> - - { - setBitbucketTokenInputHasValue(!!value); - }} - onBitbucketHostChange={(value) => { - setBitbucketHostInputHasValue(!!value); - }} - bitbucketHostSet={existingBitbucketHost} - /> - - { - setBitbucketDCTokenInputHasValue(!!value); - }} - onBitbucketDCHostChange={(value) => { - setBitbucketDCHostInputHasValue(!!value); - }} - bitbucketDCHostSet={existingBitbucketDCHost} - /> - - { - setAzureDevOpsTokenInputHasValue(!!value); - }} - onAzureDevOpsHostChange={(value) => { - setAzureDevOpsHostInputHasValue(!!value); - }} - azureDevOpsHostSet={existingAzureDevOpsHost} - /> - - { - setForgejoTokenInputHasValue(!!value); - }} - onForgejoHostChange={(value) => { - setForgejoHostInputHasValue(!!value); - }} - forgejoHostSet={existingForgejoHost} - /> -
- )} - - {isLoading && } - -
- - {t(I18nKey.GIT$DISCONNECT_TOKENS)} - - - {!isPending && t("SETTINGS$SAVE_CHANGES")} - {isPending && t("SETTINGS$SAVING")} - -
- - ); -} - -export default GitSettingsScreen; diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index 6647f9e45f..061fe01156 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -3,7 +3,6 @@ import { Outlet, redirect, useLocation, useMatches } from "react-router"; import { useTranslation } from "react-i18next"; import { Route } from "./+types/settings"; import OptionService from "#/api/option-service/option-service.api"; -import { getActiveBackend } from "#/api/backend-registry/active-store"; import { queryClient } from "#/query-client-config"; import { SettingsLayout } from "#/components/features/settings"; import { WebClientConfig } from "#/api/option-service/option.types"; @@ -13,7 +12,6 @@ import { BackendSyncedSettingsBadge } from "#/components/features/settings/backe import { useSettingsNavItems } from "#/hooks/use-settings-nav-items"; import { getFirstAvailablePath, - isLocalOnlySettingsPath, isSettingsPageHidden, } from "#/utils/settings-utils"; @@ -21,15 +19,6 @@ export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => { const url = new URL(request.url); const { pathname } = url; - // Cloud backends hide local-only settings pages. Block direct URL - // navigation, not just the menu link. - if ( - getActiveBackend().backend.kind === "cloud" && - isLocalOnlySettingsPath(pathname) - ) { - return redirect("/settings"); - } - const config = await queryClient.fetchQuery({ queryKey: QUERY_KEYS.WEB_CLIENT_CONFIG, queryFn: OptionService.getConfig, diff --git a/src/utils/settings-utils.ts b/src/utils/settings-utils.ts index 270c46dcd9..091cba51d3 100644 --- a/src/utils/settings-utils.ts +++ b/src/utils/settings-utils.ts @@ -54,36 +54,15 @@ export function isSettingsPageHidden( featureFlags: WebClientFeatureFlags | undefined, ): boolean { if (featureFlags?.hide_llm_settings && path === "/settings") return true; - if ( - featureFlags?.hide_integrations_page && - path === "/settings/integrations" - ) { - return true; - } return false; } -// Settings sub-pages that only make sense against a local agent-server. -// Hidden from navigation and blocked at the route loader when the active -// backend is a cloud SaaS environment. -export const LOCAL_ONLY_SETTINGS_PATHS = new Set([ - "/settings/integrations", -]); - -export function isLocalOnlySettingsPath(path: string): boolean { - return LOCAL_ONLY_SETTINGS_PATHS.has(path); -} - export function getFirstAvailablePath( featureFlags: WebClientFeatureFlags | undefined, ): string | null { const fallbackOrder = [ { path: "/settings", hidden: !!featureFlags?.hide_llm_settings }, { path: "/settings/mcp", hidden: false }, - { - path: "/settings/integrations", - hidden: !!featureFlags?.hide_integrations_page, - }, { path: "/settings/app", hidden: false }, { path: "/settings/secrets", hidden: false }, ];