feat: Add Close Issue Action
This commit is contained in:
parent
d85912c4c9
commit
7b20d2c26d
|
|
@ -3247,7 +3247,7 @@ export default function BoardDetailPage() {
|
|||
|
||||
{canRead && boardId ? (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<BoardForgejoIssuesPanel boardId={boardId} />
|
||||
<BoardForgejoIssuesPanel boardId={boardId} canClose={canWrite} />
|
||||
<BoardForgejoRepositoryLinks boardId={boardId} canWrite={canWrite} />
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ import { AlertCircle } from "lucide-react";
|
|||
|
||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
type getMyMembershipApiV1OrganizationsMeMemberGetResponse,
|
||||
useGetMyMembershipApiV1OrganizationsMeMemberGet,
|
||||
} from "@/api/generated/organizations/organizations";
|
||||
import {
|
||||
getForgejoIssues,
|
||||
getForgejoRepositories,
|
||||
|
|
@ -79,6 +84,15 @@ export default function GitIssuesPage() {
|
|||
parsePositiveInteger(searchParams.get("page")),
|
||||
);
|
||||
const limit = 30;
|
||||
const membershipQuery = useGetMyMembershipApiV1OrganizationsMeMemberGet<
|
||||
getMyMembershipApiV1OrganizationsMeMemberGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
query: {
|
||||
enabled: true,
|
||||
refetchOnMount: "always",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
|
@ -158,6 +172,19 @@ export default function GitIssuesPage() {
|
|||
|
||||
const staleCutoffMs = useMemo(() => Date.now() - STALE_ISSUE_MS, []);
|
||||
const recentClosedCutoffMs = useMemo(() => Date.now() - RECENT_CLOSED_MS, []);
|
||||
const canCloseIssues = useMemo(() => {
|
||||
if (membershipQuery.data?.status !== 200) {
|
||||
return false;
|
||||
}
|
||||
const member = membershipQuery.data.data;
|
||||
if (["owner", "admin"].includes(member.role)) {
|
||||
return true;
|
||||
}
|
||||
if (member.all_boards_write) {
|
||||
return true;
|
||||
}
|
||||
return (member.board_access ?? []).some((entry) => entry.can_write);
|
||||
}, [membershipQuery.data]);
|
||||
const visibleIssues = useMemo(() => {
|
||||
if (staleOnly) {
|
||||
return issues.filter((issue) => isStaleOpenIssue(issue, staleCutoffMs));
|
||||
|
|
@ -263,6 +290,7 @@ export default function GitIssuesPage() {
|
|||
issues={visibleIssues}
|
||||
repositories={repos}
|
||||
isLoading={isLoadingIssues}
|
||||
canClose={canCloseIssues}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,18 @@ import {
|
|||
getForgejoIssues,
|
||||
type ForgejoIssue,
|
||||
type ForgejoRepository,
|
||||
type ForgejoIssueListResponse,
|
||||
} from "@/lib/api-forgejo";
|
||||
import { ForgejoIssuesTable } from "@/components/git/ForgejoIssuesTable";
|
||||
|
||||
interface BoardForgejoIssuesPanelProps {
|
||||
boardId: string;
|
||||
canClose?: boolean;
|
||||
repositories?: ForgejoRepository[];
|
||||
}
|
||||
|
||||
export function BoardForgejoIssuesPanel({
|
||||
boardId,
|
||||
canClose = false,
|
||||
repositories = [],
|
||||
}: BoardForgejoIssuesPanelProps) {
|
||||
const [issues, setIssues] = useState<ForgejoIssue[]>([]);
|
||||
|
|
@ -36,7 +37,7 @@ export function BoardForgejoIssuesPanel({
|
|||
try {
|
||||
const [linksResult, issuesResult] = await Promise.all([
|
||||
getBoardForgejoRepositories(boardId),
|
||||
getForgejoIssues({ board_id: boardId, state: "open", limit: 50 }) as Promise<ForgejoIssueListResponse>,
|
||||
getForgejoIssues({ board_id: boardId, state: "open", limit: 50 }),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
const links = Array.isArray(linksResult)
|
||||
|
|
@ -70,7 +71,7 @@ export function BoardForgejoIssuesPanel({
|
|||
board_id: boardId,
|
||||
state: "open",
|
||||
limit: 50,
|
||||
}) as ForgejoIssueListResponse;
|
||||
});
|
||||
setIssues(issuesResult.items);
|
||||
} catch (err) {
|
||||
setError(
|
||||
|
|
@ -124,6 +125,7 @@ export function BoardForgejoIssuesPanel({
|
|||
issues={issues}
|
||||
repositories={repositories}
|
||||
isLoading={isLoading}
|
||||
canClose={canClose}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { closeForgejoIssue } from "@/lib/api-forgejo";
|
|||
|
||||
type CloseForgejoIssueDialogProps = {
|
||||
issue: ForgejoIssue | null;
|
||||
repositoryName: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onCloseSuccess: () => void;
|
||||
|
|
@ -23,6 +24,7 @@ type CloseForgejoIssueDialogProps = {
|
|||
|
||||
export function CloseForgejoIssueDialog({
|
||||
issue,
|
||||
repositoryName,
|
||||
open,
|
||||
onOpenChange,
|
||||
onCloseSuccess,
|
||||
|
|
@ -54,12 +56,12 @@ export function CloseForgejoIssueDialog({
|
|||
<DialogHeader>
|
||||
<DialogTitle>Close Git Project issue</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pipeline will mark issue{" "}
|
||||
Confirm closing{" "}
|
||||
<span className="font-mono font-semibold text-strong">
|
||||
#{issue.forgejo_issue_number}
|
||||
</span>{" "}
|
||||
as closed in the connected Git provider and refresh the local issue
|
||||
cache.
|
||||
{repositoryName}#{issue.forgejo_issue_number}
|
||||
</span>
|
||||
. Pipeline will close it in the connected Git provider and refresh
|
||||
the local issue cache.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-3">
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { CircleDot, ExternalLink } from "lucide-react";
|
||||
import { CircleDot, ExternalLink, XCircle } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DataTable } from "@/components/tables/DataTable";
|
||||
import type { ForgejoIssue, ForgejoIssueLabel } from "@/lib/api-forgejo";
|
||||
import type { ForgejoRepository } from "@/lib/api-forgejo";
|
||||
import { CloseForgejoIssueDialog } from "@/components/git/CloseForgejoIssueDialog";
|
||||
|
||||
/** Normalize a Forgejo label color to a valid 6-char hex string or null. */
|
||||
function normalizeLabelColor(raw: string | null | undefined): string | null {
|
||||
|
|
@ -55,6 +57,7 @@ export type ForgejoIssuesTableProps = {
|
|||
issues: ForgejoIssue[];
|
||||
repositories: ForgejoRepository[];
|
||||
isLoading?: boolean;
|
||||
canClose?: boolean;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -62,8 +65,12 @@ export function ForgejoIssuesTable({
|
|||
issues,
|
||||
repositories,
|
||||
isLoading = false,
|
||||
onRefresh: _onRefresh,
|
||||
canClose = false,
|
||||
onRefresh,
|
||||
}: ForgejoIssuesTableProps) {
|
||||
const [issueToClose, setIssueToClose] = useState<ForgejoIssue | null>(null);
|
||||
const [isCloseDialogOpen, setIsCloseDialogOpen] = useState(false);
|
||||
|
||||
const repositoryNameById = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const repository of repositories) {
|
||||
|
|
@ -74,7 +81,6 @@ export function ForgejoIssuesTable({
|
|||
}
|
||||
return map;
|
||||
}, [repositories]);
|
||||
|
||||
const columns: ColumnDef<ForgejoIssue>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
|
@ -189,8 +195,37 @@ export function ForgejoIssuesTable({
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const issue = row.original;
|
||||
const canShowClose =
|
||||
canClose && issue.state === "open" && !issue.is_pull_request;
|
||||
if (!canShowClose) {
|
||||
return null;
|
||||
}
|
||||
const repositoryName =
|
||||
repositoryNameById.get(issue.repository_id) ?? issue.repository_id;
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-[color:var(--danger)] hover:bg-[color:rgba(248,113,113,0.08)]"
|
||||
title={`Close ${repositoryName}#${issue.forgejo_issue_number}`}
|
||||
onClick={() => {
|
||||
setIssueToClose(issue);
|
||||
setIsCloseDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[repositoryNameById],
|
||||
[canClose, repositoryNameById],
|
||||
);
|
||||
const table = useReactTable({
|
||||
data: issues,
|
||||
|
|
@ -214,6 +249,23 @@ export function ForgejoIssuesTable({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<CloseForgejoIssueDialog
|
||||
issue={issueToClose}
|
||||
repositoryName={
|
||||
issueToClose
|
||||
? (repositoryNameById.get(issueToClose.repository_id) ??
|
||||
issueToClose.repository_id)
|
||||
: "Repository"
|
||||
}
|
||||
open={isCloseDialogOpen}
|
||||
onOpenChange={(nextOpen) => {
|
||||
setIsCloseDialogOpen(nextOpen);
|
||||
if (!nextOpen) {
|
||||
setIssueToClose(null);
|
||||
}
|
||||
}}
|
||||
onCloseSuccess={onRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue