From 72631320fd56fc15ef22e097f19096aa6a959d77 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Sun, 11 Jan 2026 15:21:59 +0000 Subject: [PATCH] delete issue functionality --- .../src/components/issue-detail-pane.tsx | 50 +++++++++++++++++-- .../frontend/src/lib/server/issue/delete.ts | 30 +++++++++++ .../frontend/src/lib/server/issue/index.ts | 1 + packages/frontend/src/pages/App.tsx | 7 +++ todo.md | 1 - 5 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 packages/frontend/src/lib/server/issue/delete.ts diff --git a/packages/frontend/src/components/issue-detail-pane.tsx b/packages/frontend/src/components/issue-detail-pane.tsx index 180f352..11af218 100644 --- a/packages/frontend/src/components/issue-detail-pane.tsx +++ b/packages/frontend/src/components/issue-detail-pane.tsx @@ -1,5 +1,5 @@ import type { IssueResponse, ProjectResponse, UserRecord } from "@issue/shared"; -import { X } from "lucide-react"; +import { Trash, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useSession } from "@/components/session-provider"; import SmallUserDisplay from "@/components/small-user-display"; @@ -7,6 +7,7 @@ import { StatusSelect } from "@/components/status-select"; import StatusTag from "@/components/status-tag"; import { TimerModal } from "@/components/timer-modal"; import { Button } from "@/components/ui/button"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { SelectTrigger } from "@/components/ui/select"; import { UserSelect } from "@/components/user-select"; import { issue } from "@/lib/server"; @@ -19,6 +20,7 @@ export function IssueDetailPane({ statuses, close, onIssueUpdate, + onIssueDelete, }: { project: ProjectResponse; issueData: IssueResponse; @@ -26,12 +28,14 @@ export function IssueDetailPane({ statuses: Record; close: () => void; onIssueUpdate?: () => void; + onIssueDelete?: (issueId: number) => void | Promise; }) { const { user } = useSession(); const [assigneeId, setAssigneeId] = useState( issueData.Issue.assigneeId?.toString() ?? "unassigned", ); const [status, setStatus] = useState(issueData.Issue.status); + const [deleteOpen, setDeleteOpen] = useState(false); useEffect(() => { setAssigneeId(issueData.Issue.assigneeId?.toString() ?? "unassigned"); @@ -71,6 +75,23 @@ export function IssueDetailPane({ }); }; + const handleDelete = () => { + setDeleteOpen(true); + }; + + const handleConfirmDelete = async () => { + await issue.delete({ + issueId: issueData.Issue.id, + onSuccess: async () => { + await onIssueDelete?.(issueData.Issue.id); + }, + onError: (error) => { + console.error("error deleting issue:", error); + }, + }); + setDeleteOpen(false); + }; + return (
@@ -79,10 +100,19 @@ export function IssueDetailPane({ {issueID(project.Project.key, issueData.Issue.number)}

- - +
+ + +
@@ -134,6 +164,16 @@ export function IssueDetailPane({
)} +
); diff --git a/packages/frontend/src/lib/server/issue/delete.ts b/packages/frontend/src/lib/server/issue/delete.ts new file mode 100644 index 0000000..9d3ff15 --- /dev/null +++ b/packages/frontend/src/lib/server/issue/delete.ts @@ -0,0 +1,30 @@ +import { getCsrfToken, getServerURL } from "@/lib/utils"; +import type { ServerQueryInput } from ".."; + +export async function remove({ + issueId, + onSuccess, + onError, +}: { + issueId: number; +} & ServerQueryInput) { + const url = new URL(`${getServerURL()}/issue/delete`); + url.searchParams.set("id", `${issueId}`); + + const csrfToken = getCsrfToken(); + const headers: HeadersInit = {}; + if (csrfToken) headers["X-CSRF-Token"] = csrfToken; + + const res = await fetch(url.toString(), { + headers, + credentials: "include", + }); + + if (!res.ok) { + const error = await res.text(); + onError?.(error || `failed to delete issue (${res.status})`); + } else { + const data = await res.text(); + onSuccess?.(data, res); + } +} diff --git a/packages/frontend/src/lib/server/issue/index.ts b/packages/frontend/src/lib/server/issue/index.ts index fe59352..169c76f 100644 --- a/packages/frontend/src/lib/server/issue/index.ts +++ b/packages/frontend/src/lib/server/issue/index.ts @@ -1,5 +1,6 @@ export { byProject } from "@/lib/server/issue/byProject"; export { create } from "@/lib/server/issue/create"; +export { remove as delete } from "@/lib/server/issue/delete"; export { replaceStatus } from "@/lib/server/issue/replaceStatus"; export { statusCount } from "@/lib/server/issue/statusCount"; export { update } from "@/lib/server/issue/update"; diff --git a/packages/frontend/src/pages/App.tsx b/packages/frontend/src/pages/App.tsx index d4fcd33..4258826 100644 --- a/packages/frontend/src/pages/App.tsx +++ b/packages/frontend/src/pages/App.tsx @@ -191,6 +191,12 @@ export default function App() { } }; + const handleIssueDelete = async (issueId: number) => { + setSelectedIssue(null); + setIssues((prev) => prev.filter((issue) => issue.Issue.id !== issueId)); + await refetchIssues(); + }; + // fetch issues when project is selected useEffect(() => { if (!selectedProject) return; @@ -318,6 +324,7 @@ export default function App() { statuses={selectedOrganisation.Organisation.statuses} close={() => setSelectedIssue(null)} onIssueUpdate={refetchIssues} + onIssueDelete={handleIssueDelete} /> diff --git a/todo.md b/todo.md index f706f52..6b2a168 100644 --- a/todo.md +++ b/todo.md @@ -13,7 +13,6 @@ - admins are capable of deleting comments from members who are at their permission level or below (not sure if this should apply, or if ANYONE should have control over others' comments - people in an org tend to be trusted to not be trolls) - sprint - more than one assignee - - delete issue - edit title & description - time tracking: - add current work time on detail pane for issues with time tracked