mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
4 Commits
enterprise
...
feature/gi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46dde54230 | ||
|
|
6185b7e419 | ||
|
|
d62e857c44 | ||
|
|
ad47b3d590 |
174
frontend/__tests__/github-issues-prs.test.ts
Normal file
174
frontend/__tests__/github-issues-prs.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import {
|
||||
useGitHubIssuesPRs,
|
||||
useRefreshGitHubIssuesPRs,
|
||||
} from "../src/hooks/query/use-github-issues-prs";
|
||||
import { useShouldShowUserFeatures } from "../src/hooks/use-should-show-user-features";
|
||||
import GitHubIssuesPRsService from "../src/api/github-service/github-issues-prs.api";
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock("../src/hooks/use-should-show-user-features");
|
||||
vi.mock("../src/api/github-service/github-issues-prs.api", () => ({
|
||||
default: {
|
||||
getGitHubItems: vi.fn(),
|
||||
buildItemUrl: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseShouldShowUserFeatures = vi.mocked(useShouldShowUserFeatures);
|
||||
const mockGetGitHubItems = vi.mocked(GitHubIssuesPRsService.getGitHubItems);
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
describe("useGitHubIssuesPRs", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockUseShouldShowUserFeatures.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("should be disabled when useShouldShowUserFeatures returns false", () => {
|
||||
mockUseShouldShowUserFeatures.mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useGitHubIssuesPRs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it("should be enabled when useShouldShowUserFeatures returns true", () => {
|
||||
mockUseShouldShowUserFeatures.mockReturnValue(true);
|
||||
mockGetGitHubItems.mockResolvedValue({
|
||||
items: [],
|
||||
cached_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGitHubIssuesPRs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// When enabled, the query should be loading/fetching
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
|
||||
it("should fetch and return GitHub items", async () => {
|
||||
mockUseShouldShowUserFeatures.mockReturnValue(true);
|
||||
const mockItems = [
|
||||
{
|
||||
git_provider: "github" as const,
|
||||
item_type: "issue" as const,
|
||||
status: "OPEN_ISSUE" as const,
|
||||
repo: "test/repo",
|
||||
number: 1,
|
||||
title: "Test Issue",
|
||||
author: "testuser",
|
||||
assignees: ["testuser"],
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
url: "https://github.com/test/repo/issues/1",
|
||||
},
|
||||
];
|
||||
mockGetGitHubItems.mockResolvedValue({
|
||||
items: mockItems,
|
||||
cached_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGitHubIssuesPRs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data?.items).toEqual(mockItems);
|
||||
});
|
||||
|
||||
it("should filter by item type", async () => {
|
||||
mockUseShouldShowUserFeatures.mockReturnValue(true);
|
||||
const mockItems = [
|
||||
{
|
||||
git_provider: "github" as const,
|
||||
item_type: "issue" as const,
|
||||
status: "OPEN_ISSUE" as const,
|
||||
repo: "test/repo",
|
||||
number: 1,
|
||||
title: "Test Issue",
|
||||
author: "testuser",
|
||||
assignees: ["testuser"],
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
url: "https://github.com/test/repo/issues/1",
|
||||
},
|
||||
];
|
||||
mockGetGitHubItems.mockResolvedValue({
|
||||
items: mockItems,
|
||||
cached_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useGitHubIssuesPRs({ itemType: "issues" }),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockGetGitHubItems).toHaveBeenCalledWith({ itemType: "issues" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("useRefreshGitHubIssuesPRs", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("should clear localStorage cache when called", () => {
|
||||
// Set up some cached data
|
||||
localStorage.setItem(
|
||||
"github-issues-prs-cache",
|
||||
JSON.stringify({
|
||||
data: { items: [], cached_at: new Date().toISOString() },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useRefreshGitHubIssuesPRs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Call the refresh function
|
||||
result.current();
|
||||
|
||||
// Check that localStorage was cleared
|
||||
expect(localStorage.getItem("github-issues-prs-cache")).toBeNull();
|
||||
});
|
||||
});
|
||||
316
frontend/__tests__/routes/github-issues-prs.test.tsx
Normal file
316
frontend/__tests__/routes/github-issues-prs.test.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter, Routes, Route } from "react-router";
|
||||
import GitHubIssuesPRsPage from "#/routes/github-issues-prs";
|
||||
import GitHubIssuesPRsService from "#/api/github-service/github-issues-prs.api";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import { useShouldShowUserFeatures } from "#/hooks/use-should-show-user-features";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
// Mock the services
|
||||
vi.mock("#/api/github-service/github-issues-prs.api", () => ({
|
||||
default: {
|
||||
getGitHubItems: vi.fn(),
|
||||
buildItemUrl: vi.fn((provider, repo, number, type) => {
|
||||
if (provider === "github") {
|
||||
return `https://github.com/${repo}/${type === "issue" ? "issues" : "pull"}/${number}`;
|
||||
}
|
||||
return "";
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/api/conversation-service/conversation-service.api", () => ({
|
||||
default: {
|
||||
searchConversations: vi.fn(),
|
||||
createConversation: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-should-show-user-features", () => ({
|
||||
useShouldShowUserFeatures: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-user-providers", () => ({
|
||||
useUserProviders: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock react-i18next to return the key as the translation
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: { language: "en" },
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockGetGitHubItems = vi.mocked(GitHubIssuesPRsService.getGitHubItems);
|
||||
const mockSearchConversations = vi.mocked(
|
||||
ConversationService.searchConversations,
|
||||
);
|
||||
const mockUseShouldShowUserFeatures = vi.mocked(useShouldShowUserFeatures);
|
||||
const mockUseUserProviders = vi.mocked(useUserProviders);
|
||||
|
||||
const renderGitHubIssuesPRsPage = () =>
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<MemoryRouter initialEntries={["/github-issues-prs"]}>
|
||||
<Routes>
|
||||
<Route path="/github-issues-prs" element={<GitHubIssuesPRsPage />} />
|
||||
<Route
|
||||
path="/conversations/:conversationId"
|
||||
element={<div data-testid="conversation-screen" />}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const MOCK_GITHUB_ITEMS = [
|
||||
{
|
||||
git_provider: "github" as const,
|
||||
item_type: "issue" as const,
|
||||
status: "OPEN_ISSUE" as const,
|
||||
repo: "test/repo",
|
||||
number: 1,
|
||||
title: "Test Issue",
|
||||
author: "testuser",
|
||||
assignees: ["testuser"],
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
url: "https://github.com/test/repo/issues/1",
|
||||
},
|
||||
{
|
||||
git_provider: "github" as const,
|
||||
item_type: "pr" as const,
|
||||
status: "OPEN_PR" as const,
|
||||
repo: "test/repo",
|
||||
number: 2,
|
||||
title: "Test PR",
|
||||
author: "testuser",
|
||||
assignees: [],
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
url: "https://github.com/test/repo/pull/2",
|
||||
},
|
||||
];
|
||||
|
||||
describe("GitHubIssuesPRsPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
|
||||
mockUseShouldShowUserFeatures.mockReturnValue(true);
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: ["github"],
|
||||
isLoadingSettings: false,
|
||||
});
|
||||
|
||||
mockGetGitHubItems.mockResolvedValue({
|
||||
items: MOCK_GITHUB_ITEMS,
|
||||
cached_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
mockSearchConversations.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("should render the page title", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("GITHUB_ISSUES_PRS$TITLE")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the view type selector", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
// The view label has a colon after it
|
||||
expect(screen.getByText("GITHUB_ISSUES_PRS$VIEW:")).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render filter checkboxes", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("GITHUB_ISSUES_PRS$ASSIGNED_TO_ME"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("GITHUB_ISSUES_PRS$AUTHORED_BY_ME"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the refresh button", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("GITHUB_ISSUES_PRS$REFRESH")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display GitHub items when loaded", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Issue")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test PR")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display item status badges", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("GITHUB_ISSUES_PRS$OPEN_ISSUE"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("GITHUB_ISSUES_PRS$OPEN_PR")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display Start Session buttons for items without related conversations", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
const startButtons = screen.getAllByText(
|
||||
"GITHUB_ISSUES_PRS$START_SESSION",
|
||||
);
|
||||
expect(startButtons.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter items when view type is changed to issues", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Issue")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test PR")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Change view type to issues
|
||||
const select = screen.getByRole("combobox");
|
||||
await userEvent.selectOptions(select, "issues");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Issue")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test PR")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter items when view type is changed to PRs", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Issue")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test PR")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Change view type to PRs
|
||||
const select = screen.getByRole("combobox");
|
||||
await userEvent.selectOptions(select, "prs");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Test Issue")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Test PR")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display Resume Session button when a related conversation exists", async () => {
|
||||
mockSearchConversations.mockResolvedValue([
|
||||
{
|
||||
conversation_id: "conv-1",
|
||||
title: "Working on #1",
|
||||
selected_repository: "test/repo",
|
||||
selected_branch: "main",
|
||||
git_provider: "github",
|
||||
pr_number: [1],
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
last_updated_at: "2024-01-01T00:00:00Z",
|
||||
status: "RUNNING",
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
]);
|
||||
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("GITHUB_ISSUES_PRS$RESUME_SESSION"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show empty state when no items are found", async () => {
|
||||
mockGetGitHubItems.mockResolvedValue({
|
||||
items: [],
|
||||
cached_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("GITHUB_ISSUES_PRS$NO_ITEMS"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display View on GitHub links", async () => {
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
const viewLinks = screen.getAllByText("GITHUB_ISSUES_PRS$VIEW_ON_GITHUB");
|
||||
expect(viewLinks.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show loading state while checking settings", async () => {
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: [],
|
||||
isLoadingSettings: true,
|
||||
});
|
||||
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
// Should show loading spinner
|
||||
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show no-token message when GitHub token is not configured", async () => {
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: [],
|
||||
isLoadingSettings: false,
|
||||
});
|
||||
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("GITHUB_ISSUES_PRS$NO_TOKEN")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("GITHUB_ISSUES_PRS$CONFIGURE_TOKEN"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have a link to git settings when no token is configured", async () => {
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: [],
|
||||
isLoadingSettings: false,
|
||||
});
|
||||
|
||||
renderGitHubIssuesPRsPage();
|
||||
|
||||
await waitFor(() => {
|
||||
const configureLink = screen.getByText("GITHUB_ISSUES_PRS$CONFIGURE_TOKEN");
|
||||
expect(configureLink).toHaveAttribute("href", "/settings/git");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import GitHubIssuesPRsService from "../../src/api/github-service/github-issues-prs.api";
|
||||
|
||||
describe("GitHubIssuesPRsService", () => {
|
||||
describe("buildItemUrl", () => {
|
||||
it("should build correct GitHub issue URL", () => {
|
||||
const url = GitHubIssuesPRsService.buildItemUrl(
|
||||
"github",
|
||||
"owner/repo",
|
||||
123,
|
||||
"issue",
|
||||
);
|
||||
expect(url).toBe("https://github.com/owner/repo/issues/123");
|
||||
});
|
||||
|
||||
it("should build correct GitHub PR URL", () => {
|
||||
const url = GitHubIssuesPRsService.buildItemUrl(
|
||||
"github",
|
||||
"owner/repo",
|
||||
456,
|
||||
"pr",
|
||||
);
|
||||
expect(url).toBe("https://github.com/owner/repo/pull/456");
|
||||
});
|
||||
|
||||
it("should build correct GitLab issue URL", () => {
|
||||
const url = GitHubIssuesPRsService.buildItemUrl(
|
||||
"gitlab",
|
||||
"owner/repo",
|
||||
123,
|
||||
"issue",
|
||||
);
|
||||
expect(url).toBe("https://gitlab.com/owner/repo/-/issues/123");
|
||||
});
|
||||
|
||||
it("should build correct GitLab MR URL", () => {
|
||||
const url = GitHubIssuesPRsService.buildItemUrl(
|
||||
"gitlab",
|
||||
"owner/repo",
|
||||
456,
|
||||
"pr",
|
||||
);
|
||||
expect(url).toBe("https://gitlab.com/owner/repo/-/merge_requests/456");
|
||||
});
|
||||
|
||||
it("should build correct Bitbucket issue URL", () => {
|
||||
const url = GitHubIssuesPRsService.buildItemUrl(
|
||||
"bitbucket",
|
||||
"owner/repo",
|
||||
123,
|
||||
"issue",
|
||||
);
|
||||
expect(url).toBe("https://bitbucket.org/owner/repo/issues/123");
|
||||
});
|
||||
|
||||
it("should build correct Bitbucket PR URL", () => {
|
||||
const url = GitHubIssuesPRsService.buildItemUrl(
|
||||
"bitbucket",
|
||||
"owner/repo",
|
||||
456,
|
||||
"pr",
|
||||
);
|
||||
expect(url).toBe("https://bitbucket.org/owner/repo/pull-requests/456");
|
||||
});
|
||||
|
||||
it("should return empty string for unknown provider", () => {
|
||||
const url = GitHubIssuesPRsService.buildItemUrl(
|
||||
"unknown" as any,
|
||||
"owner/repo",
|
||||
123,
|
||||
"issue",
|
||||
);
|
||||
expect(url).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
44
frontend/package-lock.json
generated
44
frontend/package-lock.json
generated
@@ -192,7 +192,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -732,7 +731,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -779,7 +777,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2331,7 +2328,6 @@
|
||||
"version": "2.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.24.tgz",
|
||||
"integrity": "sha512-9GKQgUc91otQfwmq6TLE72QKxtB341aK5NpBHS3gRoWYEuNN714Zl3OXwIZNvdXPJpsTaUo1ID1ibJU9tfgwdg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/react-utils": "2.1.14",
|
||||
"@heroui/system-rsc": "2.3.21",
|
||||
@@ -2411,7 +2407,6 @@
|
||||
"version": "2.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.24.tgz",
|
||||
"integrity": "sha512-lL+anmY4GGWwKyTbJ2PEBZE4talIZ3hu4yGpku9TktCVG2nC2YTwiWQFJ+Jcbf8Cf9vuLzI1sla5bz2jUqiBRA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/shared-utils": "2.1.12",
|
||||
"color": "^4.2.3",
|
||||
@@ -5127,7 +5122,6 @@
|
||||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
@@ -5588,7 +5582,6 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -5766,7 +5759,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -5782,7 +5774,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -5793,7 +5784,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -5834,7 +5824,6 @@
|
||||
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
@@ -5892,7 +5881,6 @@
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
@@ -6406,8 +6394,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
@@ -6435,7 +6422,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6972,7 +6958,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -7661,8 +7646,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
@@ -8380,7 +8364,6 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -8504,7 +8487,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -8585,7 +8567,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -8677,7 +8658,6 @@
|
||||
"integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"aria-query": "^5.3.2",
|
||||
"array-includes": "^3.1.8",
|
||||
@@ -8773,7 +8753,6 @@
|
||||
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.8",
|
||||
"array.prototype.findlast": "^1.2.5",
|
||||
@@ -8807,7 +8786,6 @@
|
||||
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -9076,7 +9054,6 @@
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -9407,7 +9384,6 @@
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
|
||||
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.23",
|
||||
"motion-utils": "^12.23.6",
|
||||
@@ -10075,7 +10051,6 @@
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -10853,7 +10828,6 @@
|
||||
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@@ -12555,7 +12529,6 @@
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
@@ -12650,7 +12623,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^5.0.0",
|
||||
"@mswjs/interceptors": "^0.40.0",
|
||||
@@ -13375,7 +13347,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -13437,7 +13408,6 @@
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -13634,7 +13604,6 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13683,7 +13652,6 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -13796,7 +13764,6 @@
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
|
||||
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
@@ -14158,7 +14125,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -15154,7 +15120,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
@@ -15281,7 +15246,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15583,7 +15547,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -15889,7 +15852,6 @@
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16059,7 +16021,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16072,7 +16033,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
|
||||
124
frontend/src/api/github-service/github-issues-prs.api.ts
Normal file
124
frontend/src/api/github-service/github-issues-prs.api.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export type GitHubItemType = "issue" | "pr";
|
||||
|
||||
export type GitHubItemStatus =
|
||||
| "MERGE_CONFLICTS"
|
||||
| "FAILING_CHECKS"
|
||||
| "UNRESOLVED_COMMENTS"
|
||||
| "OPEN_ISSUE"
|
||||
| "OPEN_PR";
|
||||
|
||||
export interface GitHubItem {
|
||||
git_provider: Provider;
|
||||
item_type: GitHubItemType;
|
||||
status: GitHubItemStatus;
|
||||
repo: string;
|
||||
number: number;
|
||||
title: string;
|
||||
author: string;
|
||||
assignees: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface GitHubItemsFilter {
|
||||
itemType?: "issues" | "prs" | "all";
|
||||
assignedToMe?: boolean;
|
||||
authoredByMe?: boolean;
|
||||
}
|
||||
|
||||
export interface GitHubItemsResponse {
|
||||
items: GitHubItem[];
|
||||
cached_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub Issues/PRs Service - Handles fetching GitHub issues and pull requests
|
||||
*/
|
||||
class GitHubIssuesPRsService {
|
||||
/**
|
||||
* Get GitHub issues and PRs for the authenticated user
|
||||
* This uses the existing suggested-tasks endpoint and transforms the data
|
||||
*/
|
||||
static async getGitHubItems(
|
||||
filter?: GitHubItemsFilter,
|
||||
): Promise<GitHubItemsResponse> {
|
||||
const { data } = await openHands.get<
|
||||
Array<{
|
||||
git_provider: Provider;
|
||||
task_type: GitHubItemStatus;
|
||||
repo: string;
|
||||
issue_number: number;
|
||||
title: string;
|
||||
}>
|
||||
>("/api/user/suggested-tasks");
|
||||
|
||||
// Transform the suggested tasks into GitHubItems
|
||||
const items: GitHubItem[] = data.map((task) => ({
|
||||
git_provider: task.git_provider,
|
||||
item_type: task.task_type === "OPEN_ISSUE" ? "issue" : "pr",
|
||||
status: task.task_type,
|
||||
repo: task.repo,
|
||||
number: task.issue_number,
|
||||
title: task.title,
|
||||
author: "", // Not available from suggested-tasks endpoint
|
||||
assignees: [], // Not available from suggested-tasks endpoint
|
||||
created_at: new Date().toISOString(), // Not available from suggested-tasks endpoint
|
||||
updated_at: new Date().toISOString(), // Not available from suggested-tasks endpoint
|
||||
url: GitHubIssuesPRsService.buildItemUrl(
|
||||
task.git_provider,
|
||||
task.repo,
|
||||
task.issue_number,
|
||||
task.task_type === "OPEN_ISSUE" ? "issue" : "pr",
|
||||
),
|
||||
}));
|
||||
|
||||
// Apply filters
|
||||
let filteredItems = items;
|
||||
|
||||
if (filter?.itemType === "issues") {
|
||||
filteredItems = filteredItems.filter(
|
||||
(item) => item.item_type === "issue",
|
||||
);
|
||||
} else if (filter?.itemType === "prs") {
|
||||
filteredItems = filteredItems.filter((item) => item.item_type === "pr");
|
||||
}
|
||||
|
||||
// Note: assignedToMe and authoredByMe filters would require additional API data
|
||||
// For now, the suggested-tasks endpoint already returns:
|
||||
// - PRs authored by the user
|
||||
// - Issues assigned to the user
|
||||
// So these filters are implicitly applied by the backend
|
||||
|
||||
return {
|
||||
items: filteredItems,
|
||||
cached_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL for a GitHub item
|
||||
*/
|
||||
static buildItemUrl(
|
||||
provider: Provider,
|
||||
repo: string,
|
||||
number: number,
|
||||
itemType: GitHubItemType,
|
||||
): string {
|
||||
if (provider === "github") {
|
||||
return `https://github.com/${repo}/${itemType === "issue" ? "issues" : "pull"}/${number}`;
|
||||
}
|
||||
if (provider === "gitlab") {
|
||||
return `https://gitlab.com/${repo}/-/${itemType === "issue" ? "issues" : "merge_requests"}/${number}`;
|
||||
}
|
||||
if (provider === "bitbucket") {
|
||||
return `https://bitbucket.org/${repo}/${itemType === "issue" ? "issues" : "pull-requests"}/${number}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export default GitHubIssuesPRsService;
|
||||
@@ -13,6 +13,7 @@ import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { MicroagentManagementButton } from "#/components/shared/buttons/microagent-management-button";
|
||||
import { GitHubIssuesPRsButton } from "#/components/shared/buttons/github-issues-prs-button";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
export function Sidebar() {
|
||||
@@ -85,6 +86,9 @@ export function Sidebar() {
|
||||
<MicroagentManagementButton
|
||||
disabled={settings?.email_verified === false}
|
||||
/>
|
||||
<GitHubIssuesPRsButton
|
||||
disabled={settings?.email_verified === false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row md:flex-col md:items-center gap-[26px]">
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
import PRIcon from "#/icons/u-pr.svg?react";
|
||||
|
||||
interface GitHubIssuesPRsButtonProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function GitHubIssuesPRsButton({
|
||||
disabled = false,
|
||||
}: GitHubIssuesPRsButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tooltip = t(I18nKey.SIDEBAR$GITHUB_ISSUES_PRS);
|
||||
|
||||
return (
|
||||
<TooltipButton
|
||||
tooltip={tooltip}
|
||||
ariaLabel={tooltip}
|
||||
navLinkTo="/github-issues-prs"
|
||||
testId="github-issues-prs-button"
|
||||
disabled={disabled}
|
||||
>
|
||||
<PRIcon width={28} height={28} />
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
125
frontend/src/hooks/query/use-github-issues-prs.ts
Normal file
125
frontend/src/hooks/query/use-github-issues-prs.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import GitHubIssuesPRsService, {
|
||||
GitHubItemsFilter,
|
||||
GitHubItemsResponse,
|
||||
} from "#/api/github-service/github-issues-prs.api";
|
||||
import { useShouldShowUserFeatures } from "../use-should-show-user-features";
|
||||
|
||||
const CACHE_KEY = "github-issues-prs-cache";
|
||||
const CACHE_DURATION_MS = 60 * 1000; // 1 minute
|
||||
|
||||
interface CachedData {
|
||||
data: GitHubItemsResponse;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached data from localStorage
|
||||
*/
|
||||
function getCachedData(): CachedData | null {
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (cached) {
|
||||
const parsed = JSON.parse(cached) as CachedData;
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data to localStorage cache
|
||||
*/
|
||||
function setCachedData(data: GitHubItemsResponse): void {
|
||||
try {
|
||||
const cacheEntry: CachedData = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cached data is still valid
|
||||
*/
|
||||
function isCacheValid(cached: CachedData | null): boolean {
|
||||
if (!cached) return false;
|
||||
return Date.now() - cached.timestamp < CACHE_DURATION_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch GitHub issues and PRs with local storage caching
|
||||
*/
|
||||
export const useGitHubIssuesPRs = (filter?: GitHubItemsFilter) => {
|
||||
const shouldShowUserFeatures = useShouldShowUserFeatures();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Set up auto-refresh interval
|
||||
React.useEffect(() => {
|
||||
if (!shouldShowUserFeatures) return undefined;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["github-issues-prs"] });
|
||||
}, CACHE_DURATION_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [shouldShowUserFeatures, queryClient]);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["github-issues-prs", filter],
|
||||
queryFn: async () => {
|
||||
// Check localStorage cache first
|
||||
const cached = getCachedData();
|
||||
if (isCacheValid(cached)) {
|
||||
// Return cached data but still fetch in background
|
||||
return cached!.data;
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
const response = await GitHubIssuesPRsService.getGitHubItems(filter);
|
||||
|
||||
// Save to localStorage
|
||||
setCachedData(response);
|
||||
|
||||
return response;
|
||||
},
|
||||
enabled: shouldShowUserFeatures,
|
||||
staleTime: CACHE_DURATION_MS,
|
||||
gcTime: CACHE_DURATION_MS * 5,
|
||||
// Use cached data as initial data for faster loading
|
||||
initialData: () => {
|
||||
const cached = getCachedData();
|
||||
if (cached) {
|
||||
return cached.data;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
initialDataUpdatedAt: () => {
|
||||
const cached = getCachedData();
|
||||
if (cached) {
|
||||
return cached.timestamp;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to manually refresh GitHub issues and PRs data
|
||||
*/
|
||||
export const useRefreshGitHubIssuesPRs = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return React.useCallback(() => {
|
||||
// Clear localStorage cache
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
// Invalidate React Query cache
|
||||
queryClient.invalidateQueries({ queryKey: ["github-issues-prs"] });
|
||||
}, [queryClient]);
|
||||
};
|
||||
@@ -960,4 +960,27 @@ export enum I18nKey {
|
||||
OBSERVATION_MESSAGE$SKILL_READY = "OBSERVATION_MESSAGE$SKILL_READY",
|
||||
CONVERSATION$SHOW_SKILLS = "CONVERSATION$SHOW_SKILLS",
|
||||
SKILLS_MODAL$TITLE = "SKILLS_MODAL$TITLE",
|
||||
GITHUB_ISSUES_PRS$TITLE = "GITHUB_ISSUES_PRS$TITLE",
|
||||
GITHUB_ISSUES_PRS$REFRESH = "GITHUB_ISSUES_PRS$REFRESH",
|
||||
GITHUB_ISSUES_PRS$VIEW = "GITHUB_ISSUES_PRS$VIEW",
|
||||
GITHUB_ISSUES_PRS$ALL = "GITHUB_ISSUES_PRS$ALL",
|
||||
GITHUB_ISSUES_PRS$ISSUES = "GITHUB_ISSUES_PRS$ISSUES",
|
||||
GITHUB_ISSUES_PRS$PRS = "GITHUB_ISSUES_PRS$PRS",
|
||||
GITHUB_ISSUES_PRS$ASSIGNED_TO_ME = "GITHUB_ISSUES_PRS$ASSIGNED_TO_ME",
|
||||
GITHUB_ISSUES_PRS$AUTHORED_BY_ME = "GITHUB_ISSUES_PRS$AUTHORED_BY_ME",
|
||||
GITHUB_ISSUES_PRS$LAST_UPDATED = "GITHUB_ISSUES_PRS$LAST_UPDATED",
|
||||
GITHUB_ISSUES_PRS$ERROR_LOADING = "GITHUB_ISSUES_PRS$ERROR_LOADING",
|
||||
GITHUB_ISSUES_PRS$TRY_AGAIN = "GITHUB_ISSUES_PRS$TRY_AGAIN",
|
||||
GITHUB_ISSUES_PRS$NO_ITEMS = "GITHUB_ISSUES_PRS$NO_ITEMS",
|
||||
GITHUB_ISSUES_PRS$MERGE_CONFLICTS = "GITHUB_ISSUES_PRS$MERGE_CONFLICTS",
|
||||
GITHUB_ISSUES_PRS$FAILING_CHECKS = "GITHUB_ISSUES_PRS$FAILING_CHECKS",
|
||||
GITHUB_ISSUES_PRS$UNRESOLVED_COMMENTS = "GITHUB_ISSUES_PRS$UNRESOLVED_COMMENTS",
|
||||
GITHUB_ISSUES_PRS$OPEN_ISSUE = "GITHUB_ISSUES_PRS$OPEN_ISSUE",
|
||||
GITHUB_ISSUES_PRS$OPEN_PR = "GITHUB_ISSUES_PRS$OPEN_PR",
|
||||
GITHUB_ISSUES_PRS$VIEW_ON_GITHUB = "GITHUB_ISSUES_PRS$VIEW_ON_GITHUB",
|
||||
GITHUB_ISSUES_PRS$START_SESSION = "GITHUB_ISSUES_PRS$START_SESSION",
|
||||
GITHUB_ISSUES_PRS$RESUME_SESSION = "GITHUB_ISSUES_PRS$RESUME_SESSION",
|
||||
GITHUB_ISSUES_PRS$NO_TOKEN = "GITHUB_ISSUES_PRS$NO_TOKEN",
|
||||
GITHUB_ISSUES_PRS$CONFIGURE_TOKEN = "GITHUB_ISSUES_PRS$CONFIGURE_TOKEN",
|
||||
SIDEBAR$GITHUB_ISSUES_PRS = "SIDEBAR$GITHUB_ISSUES_PRS",
|
||||
}
|
||||
|
||||
@@ -15358,5 +15358,373 @@
|
||||
"es": "Habilidades disponibles",
|
||||
"tr": "Kullanılabilir yetenekler",
|
||||
"uk": "Доступні навички"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$TITLE": {
|
||||
"en": "GitHub Issues & Pull Requests",
|
||||
"ja": "GitHub Issues & Pull Requests",
|
||||
"zh-CN": "GitHub Issues & Pull Requests",
|
||||
"zh-TW": "GitHub Issues & Pull Requests",
|
||||
"ko-KR": "GitHub Issues & Pull Requests",
|
||||
"no": "GitHub Issues & Pull Requests",
|
||||
"it": "GitHub Issues & Pull Requests",
|
||||
"pt": "GitHub Issues & Pull Requests",
|
||||
"es": "GitHub Issues & Pull Requests",
|
||||
"ar": "GitHub Issues & Pull Requests",
|
||||
"fr": "GitHub Issues & Pull Requests",
|
||||
"tr": "GitHub Issues & Pull Requests",
|
||||
"de": "GitHub Issues & Pull Requests",
|
||||
"uk": "GitHub Issues & Pull Requests"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$REFRESH": {
|
||||
"en": "Refresh",
|
||||
"ja": "更新",
|
||||
"zh-CN": "刷新",
|
||||
"zh-TW": "重新整理",
|
||||
"ko-KR": "새로고침",
|
||||
"no": "Oppdater",
|
||||
"it": "Aggiorna",
|
||||
"pt": "Atualizar",
|
||||
"es": "Actualizar",
|
||||
"ar": "تحديث",
|
||||
"fr": "Actualiser",
|
||||
"tr": "Yenile",
|
||||
"de": "Aktualisieren",
|
||||
"uk": "Оновити"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$VIEW": {
|
||||
"en": "View",
|
||||
"ja": "表示",
|
||||
"zh-CN": "查看",
|
||||
"zh-TW": "檢視",
|
||||
"ko-KR": "보기",
|
||||
"no": "Vis",
|
||||
"it": "Visualizza",
|
||||
"pt": "Ver",
|
||||
"es": "Ver",
|
||||
"ar": "عرض",
|
||||
"fr": "Voir",
|
||||
"tr": "Görüntüle",
|
||||
"de": "Ansicht",
|
||||
"uk": "Перегляд"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$ALL": {
|
||||
"en": "All",
|
||||
"ja": "すべて",
|
||||
"zh-CN": "全部",
|
||||
"zh-TW": "全部",
|
||||
"ko-KR": "전체",
|
||||
"no": "Alle",
|
||||
"it": "Tutti",
|
||||
"pt": "Todos",
|
||||
"es": "Todos",
|
||||
"ar": "الكل",
|
||||
"fr": "Tous",
|
||||
"tr": "Tümü",
|
||||
"de": "Alle",
|
||||
"uk": "Всі"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$ISSUES": {
|
||||
"en": "Issues",
|
||||
"ja": "Issues",
|
||||
"zh-CN": "Issues",
|
||||
"zh-TW": "Issues",
|
||||
"ko-KR": "Issues",
|
||||
"no": "Issues",
|
||||
"it": "Issues",
|
||||
"pt": "Issues",
|
||||
"es": "Issues",
|
||||
"ar": "Issues",
|
||||
"fr": "Issues",
|
||||
"tr": "Issues",
|
||||
"de": "Issues",
|
||||
"uk": "Issues"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$PRS": {
|
||||
"en": "Pull Requests",
|
||||
"ja": "Pull Requests",
|
||||
"zh-CN": "Pull Requests",
|
||||
"zh-TW": "Pull Requests",
|
||||
"ko-KR": "Pull Requests",
|
||||
"no": "Pull Requests",
|
||||
"it": "Pull Requests",
|
||||
"pt": "Pull Requests",
|
||||
"es": "Pull Requests",
|
||||
"ar": "Pull Requests",
|
||||
"fr": "Pull Requests",
|
||||
"tr": "Pull Requests",
|
||||
"de": "Pull Requests",
|
||||
"uk": "Pull Requests"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$ASSIGNED_TO_ME": {
|
||||
"en": "Assigned to me",
|
||||
"ja": "自分に割り当て",
|
||||
"zh-CN": "分配给我",
|
||||
"zh-TW": "指派給我",
|
||||
"ko-KR": "나에게 할당됨",
|
||||
"no": "Tildelt meg",
|
||||
"it": "Assegnato a me",
|
||||
"pt": "Atribuído a mim",
|
||||
"es": "Asignado a mí",
|
||||
"ar": "مُعيَّن لي",
|
||||
"fr": "Assigné à moi",
|
||||
"tr": "Bana atanan",
|
||||
"de": "Mir zugewiesen",
|
||||
"uk": "Призначено мені"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$AUTHORED_BY_ME": {
|
||||
"en": "Authored by me",
|
||||
"ja": "自分が作成",
|
||||
"zh-CN": "我创建的",
|
||||
"zh-TW": "我建立的",
|
||||
"ko-KR": "내가 작성함",
|
||||
"no": "Opprettet av meg",
|
||||
"it": "Creato da me",
|
||||
"pt": "Criado por mim",
|
||||
"es": "Creado por mí",
|
||||
"ar": "من إنشائي",
|
||||
"fr": "Créé par moi",
|
||||
"tr": "Benim tarafımdan oluşturulan",
|
||||
"de": "Von mir erstellt",
|
||||
"uk": "Створено мною"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$LAST_UPDATED": {
|
||||
"en": "Last updated",
|
||||
"ja": "最終更新",
|
||||
"zh-CN": "最后更新",
|
||||
"zh-TW": "最後更新",
|
||||
"ko-KR": "마지막 업데이트",
|
||||
"no": "Sist oppdatert",
|
||||
"it": "Ultimo aggiornamento",
|
||||
"pt": "Última atualização",
|
||||
"es": "Última actualización",
|
||||
"ar": "آخر تحديث",
|
||||
"fr": "Dernière mise à jour",
|
||||
"tr": "Son güncelleme",
|
||||
"de": "Zuletzt aktualisiert",
|
||||
"uk": "Останнє оновлення"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$ERROR_LOADING": {
|
||||
"en": "Failed to load GitHub items",
|
||||
"ja": "GitHubアイテムの読み込みに失敗しました",
|
||||
"zh-CN": "加载GitHub项目失败",
|
||||
"zh-TW": "載入GitHub項目失敗",
|
||||
"ko-KR": "GitHub 항목 로드 실패",
|
||||
"no": "Kunne ikke laste GitHub-elementer",
|
||||
"it": "Impossibile caricare gli elementi GitHub",
|
||||
"pt": "Falha ao carregar itens do GitHub",
|
||||
"es": "Error al cargar elementos de GitHub",
|
||||
"ar": "فشل في تحميل عناصر GitHub",
|
||||
"fr": "Échec du chargement des éléments GitHub",
|
||||
"tr": "GitHub öğeleri yüklenemedi",
|
||||
"de": "GitHub-Elemente konnten nicht geladen werden",
|
||||
"uk": "Не вдалося завантажити елементи GitHub"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$TRY_AGAIN": {
|
||||
"en": "Try Again",
|
||||
"ja": "再試行",
|
||||
"zh-CN": "重试",
|
||||
"zh-TW": "重試",
|
||||
"ko-KR": "다시 시도",
|
||||
"no": "Prøv igjen",
|
||||
"it": "Riprova",
|
||||
"pt": "Tentar novamente",
|
||||
"es": "Intentar de nuevo",
|
||||
"ar": "حاول مرة أخرى",
|
||||
"fr": "Réessayer",
|
||||
"tr": "Tekrar dene",
|
||||
"de": "Erneut versuchen",
|
||||
"uk": "Спробувати знову"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$NO_ITEMS": {
|
||||
"en": "No issues or pull requests found",
|
||||
"ja": "IssueまたはPull Requestが見つかりません",
|
||||
"zh-CN": "未找到Issue或Pull Request",
|
||||
"zh-TW": "未找到Issue或Pull Request",
|
||||
"ko-KR": "Issue 또는 Pull Request를 찾을 수 없습니다",
|
||||
"no": "Ingen issues eller pull requests funnet",
|
||||
"it": "Nessun issue o pull request trovato",
|
||||
"pt": "Nenhum issue ou pull request encontrado",
|
||||
"es": "No se encontraron issues ni pull requests",
|
||||
"ar": "لم يتم العثور على issues أو pull requests",
|
||||
"fr": "Aucun issue ou pull request trouvé",
|
||||
"tr": "Issue veya pull request bulunamadı",
|
||||
"de": "Keine Issues oder Pull Requests gefunden",
|
||||
"uk": "Issues або Pull Requests не знайдено"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$MERGE_CONFLICTS": {
|
||||
"en": "Merge Conflicts",
|
||||
"ja": "マージコンフリクト",
|
||||
"zh-CN": "合并冲突",
|
||||
"zh-TW": "合併衝突",
|
||||
"ko-KR": "병합 충돌",
|
||||
"no": "Flettekonflikter",
|
||||
"it": "Conflitti di merge",
|
||||
"pt": "Conflitos de merge",
|
||||
"es": "Conflictos de merge",
|
||||
"ar": "تعارضات الدمج",
|
||||
"fr": "Conflits de fusion",
|
||||
"tr": "Birleştirme çakışmaları",
|
||||
"de": "Merge-Konflikte",
|
||||
"uk": "Конфлікти злиття"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$FAILING_CHECKS": {
|
||||
"en": "Failing Checks",
|
||||
"ja": "失敗したチェック",
|
||||
"zh-CN": "检查失败",
|
||||
"zh-TW": "檢查失敗",
|
||||
"ko-KR": "실패한 검사",
|
||||
"no": "Mislykkede sjekker",
|
||||
"it": "Controlli falliti",
|
||||
"pt": "Verificações falhando",
|
||||
"es": "Verificaciones fallidas",
|
||||
"ar": "فحوصات فاشلة",
|
||||
"fr": "Vérifications échouées",
|
||||
"tr": "Başarısız kontroller",
|
||||
"de": "Fehlgeschlagene Prüfungen",
|
||||
"uk": "Невдалі перевірки"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$UNRESOLVED_COMMENTS": {
|
||||
"en": "Unresolved Comments",
|
||||
"ja": "未解決のコメント",
|
||||
"zh-CN": "未解决的评论",
|
||||
"zh-TW": "未解決的評論",
|
||||
"ko-KR": "해결되지 않은 댓글",
|
||||
"no": "Uløste kommentarer",
|
||||
"it": "Commenti non risolti",
|
||||
"pt": "Comentários não resolvidos",
|
||||
"es": "Comentarios sin resolver",
|
||||
"ar": "تعليقات غير محلولة",
|
||||
"fr": "Commentaires non résolus",
|
||||
"tr": "Çözülmemiş yorumlar",
|
||||
"de": "Ungelöste Kommentare",
|
||||
"uk": "Невирішені коментарі"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$OPEN_ISSUE": {
|
||||
"en": "Open Issue",
|
||||
"ja": "オープンIssue",
|
||||
"zh-CN": "开放Issue",
|
||||
"zh-TW": "開放Issue",
|
||||
"ko-KR": "열린 Issue",
|
||||
"no": "Åpen issue",
|
||||
"it": "Issue aperto",
|
||||
"pt": "Issue aberto",
|
||||
"es": "Issue abierto",
|
||||
"ar": "Issue مفتوح",
|
||||
"fr": "Issue ouvert",
|
||||
"tr": "Açık issue",
|
||||
"de": "Offenes Issue",
|
||||
"uk": "Відкритий Issue"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$OPEN_PR": {
|
||||
"en": "Open PR",
|
||||
"ja": "オープンPR",
|
||||
"zh-CN": "开放PR",
|
||||
"zh-TW": "開放PR",
|
||||
"ko-KR": "열린 PR",
|
||||
"no": "Åpen PR",
|
||||
"it": "PR aperta",
|
||||
"pt": "PR aberto",
|
||||
"es": "PR abierto",
|
||||
"ar": "PR مفتوح",
|
||||
"fr": "PR ouverte",
|
||||
"tr": "Açık PR",
|
||||
"de": "Offener PR",
|
||||
"uk": "Відкритий PR"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$VIEW_ON_GITHUB": {
|
||||
"en": "View on GitHub",
|
||||
"ja": "GitHubで表示",
|
||||
"zh-CN": "在GitHub上查看",
|
||||
"zh-TW": "在GitHub上檢視",
|
||||
"ko-KR": "GitHub에서 보기",
|
||||
"no": "Vis på GitHub",
|
||||
"it": "Visualizza su GitHub",
|
||||
"pt": "Ver no GitHub",
|
||||
"es": "Ver en GitHub",
|
||||
"ar": "عرض على GitHub",
|
||||
"fr": "Voir sur GitHub",
|
||||
"tr": "GitHub'da görüntüle",
|
||||
"de": "Auf GitHub ansehen",
|
||||
"uk": "Переглянути на GitHub"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$START_SESSION": {
|
||||
"en": "Start Session",
|
||||
"ja": "セッションを開始",
|
||||
"zh-CN": "开始会话",
|
||||
"zh-TW": "開始會話",
|
||||
"ko-KR": "세션 시작",
|
||||
"no": "Start økt",
|
||||
"it": "Avvia sessione",
|
||||
"pt": "Iniciar sessão",
|
||||
"es": "Iniciar sesión",
|
||||
"ar": "بدء الجلسة",
|
||||
"fr": "Démarrer la session",
|
||||
"tr": "Oturumu başlat",
|
||||
"de": "Sitzung starten",
|
||||
"uk": "Почати сеанс"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$RESUME_SESSION": {
|
||||
"en": "Resume Session",
|
||||
"ja": "セッションを再開",
|
||||
"zh-CN": "恢复会话",
|
||||
"zh-TW": "恢復會話",
|
||||
"ko-KR": "세션 재개",
|
||||
"no": "Gjenoppta økt",
|
||||
"it": "Riprendi sessione",
|
||||
"pt": "Retomar sessão",
|
||||
"es": "Reanudar sesión",
|
||||
"ar": "استئناف الجلسة",
|
||||
"fr": "Reprendre la session",
|
||||
"tr": "Oturumu devam ettir",
|
||||
"de": "Sitzung fortsetzen",
|
||||
"uk": "Відновити сеанс"
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$NO_TOKEN": {
|
||||
"en": "To view your GitHub issues and pull requests, you need to configure a GitHub token.",
|
||||
"ja": "GitHubのIssueとプルリクエストを表示するには、GitHubトークンを設定する必要があります。",
|
||||
"zh-CN": "要查看您的GitHub问题和拉取请求,您需要配置GitHub令牌。",
|
||||
"zh-TW": "要查看您的GitHub問題和拉取請求,您需要配置GitHub令牌。",
|
||||
"ko-KR": "GitHub 이슈와 풀 리퀘스트를 보려면 GitHub 토큰을 구성해야 합니다.",
|
||||
"no": "For å se dine GitHub-issues og pull requests, må du konfigurere et GitHub-token.",
|
||||
"it": "Per visualizzare le tue issue e pull request di GitHub, devi configurare un token GitHub.",
|
||||
"pt": "Para ver suas issues e pull requests do GitHub, você precisa configurar um token do GitHub.",
|
||||
"es": "Para ver tus issues y pull requests de GitHub, necesitas configurar un token de GitHub.",
|
||||
"ar": "لعرض مشكلات GitHub وطلبات السحب الخاصة بك، تحتاج إلى تكوين رمز GitHub.",
|
||||
"fr": "Pour voir vos issues et pull requests GitHub, vous devez configurer un token GitHub.",
|
||||
"tr": "GitHub sorunlarınızı ve çekme isteklerinizi görüntülemek için bir GitHub belirteci yapılandırmanız gerekir.",
|
||||
"de": "Um Ihre GitHub-Issues und Pull Requests anzuzeigen, müssen Sie ein GitHub-Token konfigurieren.",
|
||||
"uk": "Щоб переглянути ваші GitHub issues та pull requests, вам потрібно налаштувати токен GitHub."
|
||||
},
|
||||
"GITHUB_ISSUES_PRS$CONFIGURE_TOKEN": {
|
||||
"en": "Configure GitHub Token",
|
||||
"ja": "GitHubトークンを設定",
|
||||
"zh-CN": "配置GitHub令牌",
|
||||
"zh-TW": "配置GitHub令牌",
|
||||
"ko-KR": "GitHub 토큰 구성",
|
||||
"no": "Konfigurer GitHub-token",
|
||||
"it": "Configura token GitHub",
|
||||
"pt": "Configurar token do GitHub",
|
||||
"es": "Configurar token de GitHub",
|
||||
"ar": "تكوين رمز GitHub",
|
||||
"fr": "Configurer le token GitHub",
|
||||
"tr": "GitHub belirtecini yapılandır",
|
||||
"de": "GitHub-Token konfigurieren",
|
||||
"uk": "Налаштувати токен GitHub"
|
||||
},
|
||||
"SIDEBAR$GITHUB_ISSUES_PRS": {
|
||||
"en": "Issues & PRs",
|
||||
"ja": "Issues & PRs",
|
||||
"zh-CN": "Issues & PRs",
|
||||
"zh-TW": "Issues & PRs",
|
||||
"ko-KR": "Issues & PRs",
|
||||
"no": "Issues & PRs",
|
||||
"it": "Issues & PRs",
|
||||
"pt": "Issues & PRs",
|
||||
"es": "Issues & PRs",
|
||||
"ar": "Issues & PRs",
|
||||
"fr": "Issues & PRs",
|
||||
"tr": "Issues & PRs",
|
||||
"de": "Issues & PRs",
|
||||
"uk": "Issues & PRs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export default [
|
||||
]),
|
||||
route("conversations/:conversationId", "routes/conversation.tsx"),
|
||||
route("microagent-management", "routes/microagent-management.tsx"),
|
||||
route("github-issues-prs", "routes/github-issues-prs.tsx"),
|
||||
route("oauth/device/verify", "routes/device-verify.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
|
||||
440
frontend/src/routes/github-issues-prs.tsx
Normal file
440
frontend/src/routes/github-issues-prs.tsx
Normal file
@@ -0,0 +1,440 @@
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import {
|
||||
useGitHubIssuesPRs,
|
||||
useRefreshGitHubIssuesPRs,
|
||||
} from "#/hooks/query/use-github-issues-prs";
|
||||
import { useSearchConversations } from "#/hooks/query/use-search-conversations";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import {
|
||||
GitHubItem,
|
||||
GitHubItemsFilter,
|
||||
} from "#/api/github-service/github-issues-prs.api";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
type ViewType = "all" | "issues" | "prs";
|
||||
|
||||
interface GitHubItemCardProps {
|
||||
item: GitHubItem;
|
||||
relatedConversation?: Conversation;
|
||||
onStartSession: () => void;
|
||||
onResumeSession: () => void;
|
||||
isStarting: boolean;
|
||||
}
|
||||
|
||||
function GitHubItemCard({
|
||||
item,
|
||||
relatedConversation,
|
||||
onStartSession,
|
||||
onResumeSession,
|
||||
isStarting,
|
||||
}: GitHubItemCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getStatusBadge = () => {
|
||||
switch (item.status) {
|
||||
case "MERGE_CONFLICTS":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs rounded bg-red-500/20 text-red-400">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$MERGE_CONFLICTS)}
|
||||
</span>
|
||||
);
|
||||
case "FAILING_CHECKS":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs rounded bg-orange-500/20 text-orange-400">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$FAILING_CHECKS)}
|
||||
</span>
|
||||
);
|
||||
case "UNRESOLVED_COMMENTS":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs rounded bg-yellow-500/20 text-yellow-400">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$UNRESOLVED_COMMENTS)}
|
||||
</span>
|
||||
);
|
||||
case "OPEN_ISSUE":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs rounded bg-green-500/20 text-green-400">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$OPEN_ISSUE)}
|
||||
</span>
|
||||
);
|
||||
case "OPEN_PR":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs rounded bg-blue-500/20 text-blue-400">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$OPEN_PR)}
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 border border-[#525252] rounded-lg bg-[#25272D] hover:bg-[#2D2F36] transition-colors">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-neutral-400">
|
||||
{item.repo}#{item.number}
|
||||
</span>
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-white truncate mb-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-400 hover:underline"
|
||||
>
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$VIEW_ON_GITHUB)}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{relatedConversation ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResumeSession}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded bg-blue-600 hover:bg-blue-700 text-white transition-colors"
|
||||
>
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$RESUME_SESSION)}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStartSession}
|
||||
disabled={isStarting}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-xs font-medium rounded transition-colors",
|
||||
isStarting
|
||||
? "bg-neutral-600 text-neutral-400 cursor-not-allowed"
|
||||
: "bg-green-600 hover:bg-green-700 text-white",
|
||||
)}
|
||||
>
|
||||
{isStarting ? (
|
||||
<LoadingSpinner size="small" />
|
||||
) : (
|
||||
t(I18nKey.GITHUB_ISSUES_PRS$START_SESSION)
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubIssuesPRsPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Check if GitHub token is configured
|
||||
const { providers, isLoadingSettings } = useUserProviders();
|
||||
const hasGitHubToken = providers.includes("github");
|
||||
|
||||
// Filter state
|
||||
const [viewType, setViewType] = React.useState<ViewType>("all");
|
||||
const [assignedToMe, setAssignedToMe] = React.useState(true);
|
||||
const [authoredByMe, setAuthoredByMe] = React.useState(true);
|
||||
|
||||
// Build filter object
|
||||
const filter: GitHubItemsFilter = React.useMemo(
|
||||
() => ({
|
||||
itemType: viewType,
|
||||
assignedToMe,
|
||||
authoredByMe,
|
||||
}),
|
||||
[viewType, assignedToMe, authoredByMe],
|
||||
);
|
||||
|
||||
// Fetch GitHub items
|
||||
const {
|
||||
data: githubData,
|
||||
isLoading,
|
||||
error,
|
||||
isFetching,
|
||||
} = useGitHubIssuesPRs(filter);
|
||||
const refreshData = useRefreshGitHubIssuesPRs();
|
||||
|
||||
// Fetch conversations to find related ones
|
||||
const { data: conversations } = useSearchConversations(
|
||||
undefined,
|
||||
undefined,
|
||||
100,
|
||||
);
|
||||
|
||||
// Create conversation mutation
|
||||
const { mutate: createConversation, isPending: isCreating } =
|
||||
useCreateConversation();
|
||||
|
||||
// Track which item is being started
|
||||
const [startingItemKey, setStartingItemKey] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Find conversation related to a GitHub item
|
||||
const findRelatedConversation = React.useCallback(
|
||||
(item: GitHubItem): Conversation | undefined => {
|
||||
if (!conversations) return undefined;
|
||||
|
||||
// Look for conversations that match the repository and have the PR number
|
||||
return conversations.find((conv) => {
|
||||
// Check if the conversation is for the same repository
|
||||
if (conv.selected_repository !== item.repo) return false;
|
||||
|
||||
// Check if the conversation has the same PR/issue number in metadata
|
||||
if (conv.pr_number?.includes(item.number)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the conversation title contains the issue/PR number
|
||||
if (conv.title.includes(`#${item.number}`)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
[conversations],
|
||||
);
|
||||
|
||||
// Handle starting a new session
|
||||
const handleStartSession = React.useCallback(
|
||||
(item: GitHubItem) => {
|
||||
const itemKey = `${item.repo}-${item.number}`;
|
||||
setStartingItemKey(itemKey);
|
||||
|
||||
// Build the initial message based on item type
|
||||
let initialMessage: string;
|
||||
if (item.item_type === "issue") {
|
||||
initialMessage = `Please help me resolve issue #${item.number} in the ${item.repo} repository.
|
||||
|
||||
First, understand the issue context by reading the issue description and any comments. Then, work on resolving the issue. If you successfully resolve it, please open a draft PR with the fix.
|
||||
|
||||
Issue: ${item.url}`;
|
||||
} else {
|
||||
initialMessage = `Please help me with PR #${item.number} in the ${item.repo} repository.
|
||||
|
||||
First, read the PR description, comments, and check the CI results. Then, address any issues found:
|
||||
- Fix failing CI checks
|
||||
- Resolve merge conflicts if any
|
||||
- Address review comments
|
||||
|
||||
PR: ${item.url}`;
|
||||
}
|
||||
|
||||
createConversation(
|
||||
{
|
||||
query: initialMessage,
|
||||
repository: {
|
||||
name: item.repo,
|
||||
gitProvider: item.git_provider,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
setStartingItemKey(null);
|
||||
navigate(`/conversations/${response.conversation_id}`);
|
||||
},
|
||||
onError: () => {
|
||||
setStartingItemKey(null);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
[createConversation, navigate],
|
||||
);
|
||||
|
||||
// Handle resuming an existing session
|
||||
const handleResumeSession = React.useCallback(
|
||||
(conversation: Conversation) => {
|
||||
navigate(`/conversations/${conversation.conversation_id}`);
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
// Filter items based on view type
|
||||
const filteredItems = React.useMemo(() => {
|
||||
if (!githubData?.items) return [];
|
||||
|
||||
let { items } = githubData;
|
||||
|
||||
if (viewType === "issues") {
|
||||
items = items.filter((item) => item.item_type === "issue");
|
||||
} else if (viewType === "prs") {
|
||||
items = items.filter((item) => item.item_type === "pr");
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [githubData?.items, viewType]);
|
||||
|
||||
// Show loading state while checking settings
|
||||
if (isLoadingSettings) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<LoadingSpinner size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show message if GitHub token is not configured
|
||||
if (!hasGitHubToken) {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center p-6">
|
||||
<div className="max-w-md text-center">
|
||||
<h1 className="text-xl font-semibold text-white mb-4">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$TITLE)}
|
||||
</h1>
|
||||
<p className="text-neutral-400 mb-6">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$NO_TOKEN)}
|
||||
</p>
|
||||
<Link
|
||||
to="/settings/git"
|
||||
className="inline-block px-4 py-2 text-sm font-medium rounded bg-blue-600 hover:bg-blue-700 text-white transition-colors"
|
||||
>
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$CONFIGURE_TOKEN)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-transparent overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-[#525252]">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold text-white">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$TITLE)}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={refreshData}
|
||||
disabled={isFetching}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-sm font-medium rounded transition-colors",
|
||||
isFetching
|
||||
? "bg-neutral-600 text-neutral-400 cursor-not-allowed"
|
||||
: "bg-neutral-700 hover:bg-neutral-600 text-white",
|
||||
)}
|
||||
>
|
||||
{isFetching ? (
|
||||
<LoadingSpinner size="small" />
|
||||
) : (
|
||||
t(I18nKey.GITHUB_ISSUES_PRS$REFRESH)
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{/* View type selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-neutral-400">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$VIEW)}:
|
||||
</span>
|
||||
<select
|
||||
value={viewType}
|
||||
onChange={(e) => setViewType(e.target.value as ViewType)}
|
||||
className="px-3 py-1.5 text-sm rounded bg-[#25272D] border border-[#525252] text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="all">{t(I18nKey.GITHUB_ISSUES_PRS$ALL)}</option>
|
||||
<option value="issues">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$ISSUES)}
|
||||
</option>
|
||||
<option value="prs">{t(I18nKey.GITHUB_ISSUES_PRS$PRS)}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assignedToMe}
|
||||
onChange={(e) => setAssignedToMe(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-[#525252] bg-[#25272D] text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
|
||||
/>
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$ASSIGNED_TO_ME)}
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={authoredByMe}
|
||||
onChange={(e) => setAuthoredByMe(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-[#525252] bg-[#25272D] text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
|
||||
/>
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$AUTHORED_BY_ME)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Cache info */}
|
||||
{githubData?.cached_at && (
|
||||
<div className="mt-2 text-xs text-neutral-500">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$LAST_UPDATED)}:{" "}
|
||||
{new Date(githubData.cached_at).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
|
||||
{isLoading && !githubData && (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<LoadingSpinner size="large" />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && error && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<p className="text-red-400 mb-2">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$ERROR_LOADING)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={refreshData}
|
||||
className="px-4 py-2 text-sm font-medium rounded bg-blue-600 hover:bg-blue-700 text-white transition-colors"
|
||||
>
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$TRY_AGAIN)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && filteredItems.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<p className="text-neutral-400">
|
||||
{t(I18nKey.GITHUB_ISSUES_PRS$NO_ITEMS)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && filteredItems.length > 0 && (
|
||||
<div className="grid gap-4 max-w-4xl mx-auto">
|
||||
{filteredItems.map((item) => {
|
||||
const itemKey = `${item.repo}-${item.number}`;
|
||||
const relatedConversation = findRelatedConversation(item);
|
||||
|
||||
return (
|
||||
<GitHubItemCard
|
||||
key={itemKey}
|
||||
item={item}
|
||||
relatedConversation={relatedConversation}
|
||||
onStartSession={() => handleStartSession(item)}
|
||||
onResumeSession={() =>
|
||||
relatedConversation &&
|
||||
handleResumeSession(relatedConversation)
|
||||
}
|
||||
isStarting={isCreating && startingItemKey === itemKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitHubIssuesPRsPage;
|
||||
Reference in New Issue
Block a user