mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
delete issue functionality
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
30
packages/frontend/src/lib/server/issue/delete.ts
Normal file
30
packages/frontend/src/lib/server/issue/delete.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
1
todo.md
1
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)
|
- 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
|
||||||
|
|||||||
Reference in New Issue
Block a user