only show status remove warning if it is being used

This commit is contained in:
Oliver Bryan
2026-01-11 00:14:06 +00:00
parent 69c8ac7bd0
commit 558d0aa3c8
8 changed files with 118 additions and 10 deletions

View File

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

View File

@@ -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))),

View File

@@ -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,

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

View File

@@ -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">

View File

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

View File

@@ -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";

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