From 558d0aa3c857d7473d431de04d34c31010e6a01d Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Sun, 11 Jan 2026 00:14:06 +0000 Subject: [PATCH] only show status remove warning if it is being used --- packages/backend/src/db/queries/issues.ts | 19 ++++++++ packages/backend/src/index.ts | 1 + packages/backend/src/routes/index.ts | 2 + .../backend/src/routes/issues/status-count.ts | 31 +++++++++++++ .../src/components/organisations-dialog.tsx | 43 +++++++++++++++---- .../frontend/src/components/status-tag.tsx | 3 +- .../frontend/src/lib/server/issue/index.ts | 1 + .../src/lib/server/issue/statusCount.ts | 28 ++++++++++++ 8 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 packages/backend/src/routes/issues/status-count.ts create mode 100644 packages/frontend/src/lib/server/issue/statusCount.ts 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?