diff --git a/backend/app/api/forgejo_issues.py b/backend/app/api/forgejo_issues.py
index bd592b9..263071d 100644
--- a/backend/app/api/forgejo_issues.py
+++ b/backend/app/api/forgejo_issues.py
@@ -34,6 +34,7 @@ async def list_issues(
session: AsyncSession = SESSION_DEP,
ctx: OrganizationContext = ORG_MEMBER_DEP,
repository_id: str | None = Query(None, description="Filter by repository ID"),
+ board_id: str | None = Query(None, description="Filter by board ID (returns issues from all repos linked to this board)"),
state: str | None = Query(None, description="Filter by state (open, closed)"),
label: str | None = Query(None, description="Filter by label name"),
assignee: str | None = Query(None, description="Filter by assignee login"),
@@ -48,6 +49,23 @@ async def list_issues(
ForgejoIssue.is_pull_request.is_(False),
)
+ if board_id:
+ try:
+ board_uuid = UUID(board_id)
+ except ValueError:
+ raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid board_id format")
+ linked_repo_ids = (
+ await session.exec(
+ select(BoardRepositoryLink.repository_id).where(
+ BoardRepositoryLink.board_id == board_uuid,
+ BoardRepositoryLink.organization_id == ctx.organization.id,
+ )
+ )
+ ).all()
+ if not linked_repo_ids:
+ return ForgejoIssueListResponse(items=[], total=0, page=page, limit=limit)
+ statement = statement.where(ForgejoIssue.repository_id.in_(linked_repo_ids))
+
if repository_id:
try:
repo_uuid = UUID(repository_id)
@@ -82,6 +100,23 @@ async def list_issues(
ForgejoIssue.organization_id == ctx.organization.id,
ForgejoIssue.is_pull_request.is_(False),
)
+ if board_id:
+ try:
+ board_uuid = UUID(board_id)
+ linked_repo_ids_for_count = (
+ await session.exec(
+ select(BoardRepositoryLink.repository_id).where(
+ BoardRepositoryLink.board_id == board_uuid,
+ BoardRepositoryLink.organization_id == ctx.organization.id,
+ )
+ )
+ ).all()
+ if linked_repo_ids_for_count:
+ total_statement = total_statement.where(
+ ForgejoIssue.repository_id.in_(linked_repo_ids_for_count)
+ )
+ except ValueError:
+ pass
if repository_id:
try:
repo_uuid = UUID(repository_id)
diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx
index 32a7615..2ea3c85 100644
--- a/frontend/src/app/boards/[boardId]/page.tsx
+++ b/frontend/src/app/boards/[boardId]/page.tsx
@@ -37,6 +37,7 @@ import { DashboardShell } from "@/components/templates/DashboardShell";
import { BoardChatComposer } from "@/components/BoardChatComposer";
import { TaskCustomFieldsEditor } from "./TaskCustomFieldsEditor";
import { BoardForgejoRepositoryLinks } from "@/components/git/BoardForgejoRepositoryLinks";
+import { BoardForgejoIssuesPanel } from "@/components/git/BoardForgejoIssuesPanel";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -3245,7 +3246,8 @@ export default function BoardDetailPage() {
{canRead && boardId ? (
-
+
+
) : null}
diff --git a/frontend/src/components/git/BoardForgejoIssuesPanel.tsx b/frontend/src/components/git/BoardForgejoIssuesPanel.tsx
new file mode 100644
index 0000000..712017e
--- /dev/null
+++ b/frontend/src/components/git/BoardForgejoIssuesPanel.tsx
@@ -0,0 +1,132 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { AlertCircle, GitBranch } from "lucide-react";
+
+import {
+ getBoardForgejoRepositories,
+ getForgejoIssues,
+ type ForgejoIssue,
+ type ForgejoRepository,
+ type ForgejoIssueListResponse,
+} from "@/lib/api-forgejo";
+import { ForgejoIssuesTable } from "@/components/git/ForgejoIssuesTable";
+
+interface BoardForgejoIssuesPanelProps {
+ boardId: string;
+ repositories?: ForgejoRepository[];
+}
+
+export function BoardForgejoIssuesPanel({
+ boardId,
+ repositories = [],
+}: BoardForgejoIssuesPanelProps) {
+ const [issues, setIssues] = useState
([]);
+ const [hasLinkedRepos, setHasLinkedRepos] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!boardId) return;
+ let cancelled = false;
+
+ const load = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const [linksResult, issuesResult] = await Promise.all([
+ getBoardForgejoRepositories(boardId),
+ getForgejoIssues({ board_id: boardId, state: "open", limit: 50 }) as Promise,
+ ]);
+ if (cancelled) return;
+ const links = Array.isArray(linksResult)
+ ? linksResult
+ : (linksResult.repositories ?? []);
+ setHasLinkedRepos(links.length > 0);
+ setIssues(issuesResult.items);
+ } catch (err) {
+ if (cancelled) return;
+ setError(
+ err instanceof Error
+ ? err.message
+ : "Could not load Git Project issues.",
+ );
+ } finally {
+ if (!cancelled) setIsLoading(false);
+ }
+ };
+
+ load();
+ return () => {
+ cancelled = true;
+ };
+ }, [boardId]);
+
+ const handleRefresh = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const issuesResult = await getForgejoIssues({
+ board_id: boardId,
+ state: "open",
+ limit: 50,
+ }) as ForgejoIssueListResponse;
+ setIssues(issuesResult.items);
+ } catch (err) {
+ setError(
+ err instanceof Error
+ ? err.message
+ : "Could not refresh Git Project issues.",
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Git Project Issues
+
+
+ Open issues from repositories linked to this board.
+
+
+ {!isLoading && issues.length > 0 && (
+
+ {issues.length} open
+
+ )}
+
+
+ {error ? (
+
+ ) : hasLinkedRepos === false && !isLoading ? (
+
+
+
+
+
+ No repositories linked
+
+
+ Link a Git Project repository to this board to see its open issues
+ here.
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/organisms/DashboardSidebar.tsx b/frontend/src/components/organisms/DashboardSidebar.tsx
index 02b9ba7..23d8ff0 100644
--- a/frontend/src/components/organisms/DashboardSidebar.tsx
+++ b/frontend/src/components/organisms/DashboardSidebar.tsx
@@ -87,7 +87,7 @@ export function DashboardSidebar() {
Live feed
@@ -102,42 +102,42 @@ export function DashboardSidebar() {
Board groups
Boards
Git Projects
Issues
Tags
Approvals
@@ -145,9 +145,7 @@ export function DashboardSidebar() {
{isAdmin ? (
Custom fields
@@ -165,19 +163,14 @@ export function DashboardSidebar() {
Marketplace
Packs
@@ -194,14 +187,14 @@ export function DashboardSidebar() {
Organization
Settings
@@ -209,7 +202,7 @@ export function DashboardSidebar() {
{isAdmin ? (
Gateways
@@ -218,7 +211,7 @@ export function DashboardSidebar() {
{isAdmin ? (
Agents
diff --git a/frontend/src/lib/api-forgejo.ts b/frontend/src/lib/api-forgejo.ts
index 2fca81b..48be17c 100644
--- a/frontend/src/lib/api-forgejo.ts
+++ b/frontend/src/lib/api-forgejo.ts
@@ -337,6 +337,7 @@ export interface ForgejoIssueListResponse {
// Forgejo Issue API
export async function getForgejoIssues(params?: {
repository_id?: string;
+ board_id?: string;
state?: string;
search?: string;
page?: number;
@@ -345,6 +346,7 @@ export async function getForgejoIssues(params?: {
const searchParams = new URLSearchParams();
if (params?.repository_id)
searchParams.set("repository_id", params.repository_id);
+ if (params?.board_id) searchParams.set("board_id", params.board_id);
if (params?.state) searchParams.set("state", params.state);
if (params?.search) searchParams.set("search", params.search);
if (params?.page) searchParams.set("page", params.page.toString());