diff --git a/packages/backend/src/db/queries/issues.ts b/packages/backend/src/db/queries/issues.ts
index ab5538e..67efdef 100644
--- a/packages/backend/src/db/queries/issues.ts
+++ b/packages/backend/src/db/queries/issues.ts
@@ -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`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");
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index de1969c..8fd7f7c 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -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))),
diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts
index 9450df9..467b750 100644
--- a/packages/backend/src/routes/index.ts
+++ b/packages/backend/src/routes/index.ts
@@ -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,
diff --git a/packages/backend/src/routes/issues/status-count.ts b/packages/backend/src/routes/issues/status-count.ts
new file mode 100644
index 0000000..5f8b185
--- /dev/null
+++ b/packages/backend/src/routes/issues/status-count.ts
@@ -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);
+}
diff --git a/packages/frontend/src/components/organisations-dialog.tsx b/packages/frontend/src/components/organisations-dialog.tsx
index 75b767d..9b63d5c 100644
--- a/packages/frontend/src/components/organisations-dialog.tsx
+++ b/packages/frontend/src/components/organisations-dialog.tsx
@@ -52,6 +52,7 @@ function OrganisationsDialog({
const [newStatusColour, setNewStatusColour] = useState(DEFAULT_STATUS_COLOUR);
const [statusError, setStatusError] = useState(null);
const [statusToRemove, setStatusToRemove] = useState(null);
+ const [issuesUsingStatus, setIssuesUsingStatus] = useState(0);
const [reassignToStatus, setReassignToStatus] = useState("");
const [confirmDialog, setConfirmDialog] = useState<{
@@ -218,11 +219,34 @@ function OrganisationsDialog({
setStatusError(null);
};
- const handleRemoveStatusClick = (status: string) => {
- if (Object.keys(statuses).length <= 1) return;
- setStatusToRemove(status);
- const remaining = Object.keys(statuses).filter((s) => s !== status);
- setReassignToStatus(remaining[0] || "");
+ 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({
)}
-
+
Organisations
@@ -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({
}
}}
>
-
+
Remove Status
@@ -596,7 +620,8 @@ function OrganisationsDialog({
{statusToRemove ? (
) : null}{" "}
- status? Which status would you like issues with this status to be set to?
+ status? {issuesUsingStatus}{" "}
+ issues are using it. Which status would you like these issues to use instead?