delete issue functionality

This commit is contained in:
Oliver Bryan
2026-01-11 15:21:59 +00:00
parent 828768ad40
commit 72631320fd
5 changed files with 83 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import type { IssueResponse, ProjectResponse, UserRecord } from "@issue/shared"; 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 { useEffect, useState } from "react";
import { useSession } from "@/components/session-provider"; import { useSession } from "@/components/session-provider";
import SmallUserDisplay from "@/components/small-user-display"; import SmallUserDisplay from "@/components/small-user-display";
@@ -7,6 +7,7 @@ import { StatusSelect } from "@/components/status-select";
import StatusTag from "@/components/status-tag"; import StatusTag from "@/components/status-tag";
import { TimerModal } from "@/components/timer-modal"; import { TimerModal } from "@/components/timer-modal";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { SelectTrigger } from "@/components/ui/select"; import { SelectTrigger } from "@/components/ui/select";
import { UserSelect } from "@/components/user-select"; import { UserSelect } from "@/components/user-select";
import { issue } from "@/lib/server"; import { issue } from "@/lib/server";
@@ -19,6 +20,7 @@ export function IssueDetailPane({
statuses, statuses,
close, close,
onIssueUpdate, onIssueUpdate,
onIssueDelete,
}: { }: {
project: ProjectResponse; project: ProjectResponse;
issueData: IssueResponse; issueData: IssueResponse;
@@ -26,12 +28,14 @@ export function IssueDetailPane({
statuses: Record<string, string>; statuses: Record<string, string>;
close: () => void; close: () => void;
onIssueUpdate?: () => void; onIssueUpdate?: () => void;
onIssueDelete?: (issueId: number) => void | Promise<void>;
}) { }) {
const { user } = useSession(); const { user } = useSession();
const [assigneeId, setAssigneeId] = useState<string>( const [assigneeId, setAssigneeId] = useState<string>(
issueData.Issue.assigneeId?.toString() ?? "unassigned", issueData.Issue.assigneeId?.toString() ?? "unassigned",
); );
const [status, setStatus] = useState<string>(issueData.Issue.status); const [status, setStatus] = useState<string>(issueData.Issue.status);
const [deleteOpen, setDeleteOpen] = useState(false);
useEffect(() => { useEffect(() => {
setAssigneeId(issueData.Issue.assigneeId?.toString() ?? "unassigned"); 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 ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-row items-center justify-end border-b h-[25px]"> <div className="flex flex-row items-center justify-end border-b h-[25px]">
@@ -79,11 +100,20 @@ export function IssueDetailPane({
{issueID(project.Project.key, issueData.Issue.number)} {issueID(project.Project.key, issueData.Issue.number)}
</p> </p>
</span> </span>
<div className="flex gap-1 items-center">
<Button
variant="dummy"
size="none"
onClick={handleDelete}
className="text-destructive hover:text-destructive/70"
>
<Trash />
</Button>
<Button variant={"dummy"} onClick={close} className="px-0 py-0 w-6 h-6"> <Button variant={"dummy"} onClick={close} className="px-0 py-0 w-6 h-6">
<X /> <X />
</Button> </Button>
</div> </div>
</div>
<div className="flex flex-col w-full p-2 py-2 gap-2"> <div className="flex flex-col w-full p-2 py-2 gap-2">
<div className="flex gap-2"> <div className="flex gap-2">
@@ -134,6 +164,16 @@ export function IssueDetailPane({
<TimerModal issueId={issueData.Issue.id} /> <TimerModal issueId={issueData.Issue.id} />
</div> </div>
)} )}
<ConfirmDialog
open={deleteOpen}
onOpenChange={setDeleteOpen}
onConfirm={handleConfirmDelete}
title="Delete issue"
message="This will permanently delete the issue."
processingText="Deleting..."
confirmText="Delete"
variant="destructive"
/>
</div> </div>
</div> </div>
); );

View File

@@ -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);
}
}

View File

@@ -1,5 +1,6 @@
export { byProject } from "@/lib/server/issue/byProject"; export { byProject } from "@/lib/server/issue/byProject";
export { create } from "@/lib/server/issue/create"; export { create } from "@/lib/server/issue/create";
export { remove as delete } from "@/lib/server/issue/delete";
export { replaceStatus } from "@/lib/server/issue/replaceStatus"; export { replaceStatus } from "@/lib/server/issue/replaceStatus";
export { statusCount } from "@/lib/server/issue/statusCount"; export { statusCount } from "@/lib/server/issue/statusCount";
export { update } from "@/lib/server/issue/update"; export { update } from "@/lib/server/issue/update";

View File

@@ -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 // fetch issues when project is selected
useEffect(() => { useEffect(() => {
if (!selectedProject) return; if (!selectedProject) return;
@@ -318,6 +324,7 @@ export default function App() {
statuses={selectedOrganisation.Organisation.statuses} statuses={selectedOrganisation.Organisation.statuses}
close={() => setSelectedIssue(null)} close={() => setSelectedIssue(null)}
onIssueUpdate={refetchIssues} onIssueUpdate={refetchIssues}
onIssueDelete={handleIssueDelete}
/> />
</div> </div>
</ResizablePanel> </ResizablePanel>

View File

@@ -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) - 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 - sprint
- more than one assignee - more than one assignee
- delete issue
- edit title & description - edit title & description
- time tracking: - time tracking:
- add current work time on detail pane for issues with time tracked - add current work time on detail pane for issues with time tracked