mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
only show status remove warning if it is being used
This commit is contained in:
@@ -71,6 +71,25 @@ export async function getIssueByNumber(projectId: number, number: number) {
|
||||
return issue;
|
||||
}
|
||||
|
||||
export async function getIssueStatusCountByOrganisation(organisationId: number, status: string) {
|
||||
const { Project } = await import("@issue/shared");
|
||||
|
||||
const projects = await db
|
||||
.select({ id: Project.id })
|
||||
.from(Project)
|
||||
.where(eq(Project.organisationId, organisationId));
|
||||
const projectIds = projects.map((p) => p.id);
|
||||
|
||||
if (projectIds.length === 0) return { count: 0 };
|
||||
|
||||
const [result] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(Issue)
|
||||
.where(and(eq(Issue.status, status), inArray(Issue.projectId, projectIds)));
|
||||
|
||||
return { count: result?.count ?? 0 };
|
||||
}
|
||||
|
||||
export async function replaceIssueStatus(organisationId: number, oldStatus: string, newStatus: string) {
|
||||
const { Project } = await import("@issue/shared");
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const main = async () => {
|
||||
|
||||
"/issues/by-project": withCors(withAuth(routes.issuesByProject)),
|
||||
"/issues/replace-status": withCors(withAuth(withCSRF(routes.issuesReplaceStatus))),
|
||||
"/issues/status-count": withCors(withAuth(routes.issuesStatusCount)),
|
||||
"/issues/all": withCors(withAuth(routes.issues)),
|
||||
|
||||
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
|
||||
|
||||
@@ -8,6 +8,7 @@ import issueUpdate from "./issue/update";
|
||||
import issues from "./issues/all";
|
||||
import issuesByProject from "./issues/by-project";
|
||||
import issuesReplaceStatus from "./issues/replace-status";
|
||||
import issuesStatusCount from "./issues/status-count";
|
||||
import organisationAddMember from "./organisation/add-member";
|
||||
import organisationById from "./organisation/by-id";
|
||||
import organisationsByUser from "./organisation/by-user";
|
||||
@@ -50,6 +51,7 @@ export const routes = {
|
||||
issuesByProject,
|
||||
issues,
|
||||
issuesReplaceStatus,
|
||||
issuesStatusCount,
|
||||
|
||||
organisationCreate,
|
||||
organisationById,
|
||||
|
||||
31
packages/backend/src/routes/issues/status-count.ts
Normal file
31
packages/backend/src/routes/issues/status-count.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { getIssueStatusCountByOrganisation, getOrganisationMemberRole } from "../../db/queries";
|
||||
|
||||
// /issues/status-count?organisationId=1&status=TODO
|
||||
export default async function issuesStatusCount(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const organisationIdParam = url.searchParams.get("organisationId");
|
||||
const status = url.searchParams.get("status");
|
||||
|
||||
if (!organisationIdParam) {
|
||||
return new Response("missing organisationId", { status: 400 });
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return new Response("missing status", { status: 400 });
|
||||
}
|
||||
|
||||
const organisationId = Number(organisationIdParam);
|
||||
if (!Number.isInteger(organisationId)) {
|
||||
return new Response("organisationId must be an integer", { status: 400 });
|
||||
}
|
||||
|
||||
const membership = await getOrganisationMemberRole(organisationId, req.userId);
|
||||
if (!membership) {
|
||||
return new Response("not a member of this organisation", { status: 403 });
|
||||
}
|
||||
|
||||
const result = await getIssueStatusCountByOrganisation(organisationId, status);
|
||||
|
||||
return Response.json(result);
|
||||
}
|
||||
@@ -52,6 +52,7 @@ function OrganisationsDialog({
|
||||
const [newStatusColour, setNewStatusColour] = useState(DEFAULT_STATUS_COLOUR);
|
||||
const [statusError, setStatusError] = useState<string | null>(null);
|
||||
const [statusToRemove, setStatusToRemove] = useState<string | null>(null);
|
||||
const [issuesUsingStatus, setIssuesUsingStatus] = useState<number>(0);
|
||||
const [reassignToStatus, setReassignToStatus] = useState<string>("");
|
||||
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
@@ -218,11 +219,34 @@ function OrganisationsDialog({
|
||||
setStatusError(null);
|
||||
};
|
||||
|
||||
const handleRemoveStatusClick = (status: string) => {
|
||||
if (Object.keys(statuses).length <= 1) return;
|
||||
const handleRemoveStatusClick = async (status: string) => {
|
||||
if (Object.keys(statuses).length <= 1 || !selectedOrganisation) return;
|
||||
try {
|
||||
await issue.statusCount({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
status,
|
||||
onSuccess: (data) => {
|
||||
const count = (data as { count?: number }).count ?? 0;
|
||||
if (count > 0) {
|
||||
setStatusToRemove(status);
|
||||
setIssuesUsingStatus(count);
|
||||
const remaining = Object.keys(statuses).filter((s) => s !== status);
|
||||
setReassignToStatus(remaining[0] || "");
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStatuses = Object.keys(statuses).filter((s) => s !== status);
|
||||
void updateStatuses(
|
||||
Object.fromEntries(nextStatuses.map((statusKey) => [statusKey, statuses[statusKey]])),
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("error checking status usage:", error);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("error checking status usage:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const moveStatus = async (status: string, direction: "up" | "down") => {
|
||||
@@ -276,7 +300,7 @@ function OrganisationsDialog({
|
||||
)}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-md w-full max-w-[calc(100vw-2rem)]">
|
||||
<DialogContent className="max-w-lg w-full max-w-[calc(100vw-2rem)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Organisations</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -471,7 +495,7 @@ function OrganisationsDialog({
|
||||
Object.keys(statuses).length <= 1
|
||||
}
|
||||
onSelect={() =>
|
||||
handleRemoveStatusClick(status)
|
||||
void handleRemoveStatusClick(status)
|
||||
}
|
||||
className="hover:bg-destructive/10"
|
||||
>
|
||||
@@ -587,7 +611,7 @@ function OrganisationsDialog({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogContent className="w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remove Status</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -596,7 +620,8 @@ function OrganisationsDialog({
|
||||
{statusToRemove ? (
|
||||
<StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />
|
||||
) : null}{" "}
|
||||
status? Which status would you like issues with this status to be set to?
|
||||
status? <span className="font-700 text-foreground">{issuesUsingStatus}</span>{" "}
|
||||
issues are using it. Which status would you like these issues to use instead?
|
||||
</p>
|
||||
<Select value={reassignToStatus} onValueChange={setReassignToStatus}>
|
||||
<SelectTrigger className="w-min">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_STATUS_COLOUR } from "@issue/shared";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DARK_TEXT_COLOUR = "#0a0a0a";
|
||||
@@ -14,7 +15,7 @@ const isLight = (hex: string): boolean => {
|
||||
|
||||
export default function StatusTag({
|
||||
status,
|
||||
colour,
|
||||
colour = DEFAULT_STATUS_COLOUR,
|
||||
className,
|
||||
}: {
|
||||
status: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { byProject } from "@/lib/server/issue/byProject";
|
||||
export { create } from "@/lib/server/issue/create";
|
||||
export { replaceStatus } from "@/lib/server/issue/replaceStatus";
|
||||
export { statusCount } from "@/lib/server/issue/statusCount";
|
||||
export { update } from "@/lib/server/issue/update";
|
||||
|
||||
28
packages/frontend/src/lib/server/issue/statusCount.ts
Normal file
28
packages/frontend/src/lib/server/issue/statusCount.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getServerURL } from "@/lib/utils";
|
||||
import type { ServerQueryInput } from "..";
|
||||
|
||||
export async function statusCount({
|
||||
organisationId,
|
||||
status,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
organisationId: number;
|
||||
status: string;
|
||||
} & ServerQueryInput) {
|
||||
const url = new URL(`${getServerURL()}/issues/status-count`);
|
||||
url.searchParams.set("organisationId", `${organisationId}`);
|
||||
url.searchParams.set("status", status);
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text();
|
||||
onError?.(error || `failed to get issue status count (${res.status})`);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
onSuccess?.(data, res);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user