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;
|
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) {
|
export async function replaceIssueStatus(organisationId: number, oldStatus: string, newStatus: string) {
|
||||||
const { Project } = await import("@issue/shared");
|
const { Project } = await import("@issue/shared");
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const main = async () => {
|
|||||||
|
|
||||||
"/issues/by-project": withCors(withAuth(routes.issuesByProject)),
|
"/issues/by-project": withCors(withAuth(routes.issuesByProject)),
|
||||||
"/issues/replace-status": withCors(withAuth(withCSRF(routes.issuesReplaceStatus))),
|
"/issues/replace-status": withCors(withAuth(withCSRF(routes.issuesReplaceStatus))),
|
||||||
|
"/issues/status-count": withCors(withAuth(routes.issuesStatusCount)),
|
||||||
"/issues/all": withCors(withAuth(routes.issues)),
|
"/issues/all": withCors(withAuth(routes.issues)),
|
||||||
|
|
||||||
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
|
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import issueUpdate from "./issue/update";
|
|||||||
import issues from "./issues/all";
|
import issues from "./issues/all";
|
||||||
import issuesByProject from "./issues/by-project";
|
import issuesByProject from "./issues/by-project";
|
||||||
import issuesReplaceStatus from "./issues/replace-status";
|
import issuesReplaceStatus from "./issues/replace-status";
|
||||||
|
import issuesStatusCount from "./issues/status-count";
|
||||||
import organisationAddMember from "./organisation/add-member";
|
import organisationAddMember from "./organisation/add-member";
|
||||||
import organisationById from "./organisation/by-id";
|
import organisationById from "./organisation/by-id";
|
||||||
import organisationsByUser from "./organisation/by-user";
|
import organisationsByUser from "./organisation/by-user";
|
||||||
@@ -50,6 +51,7 @@ export const routes = {
|
|||||||
issuesByProject,
|
issuesByProject,
|
||||||
issues,
|
issues,
|
||||||
issuesReplaceStatus,
|
issuesReplaceStatus,
|
||||||
|
issuesStatusCount,
|
||||||
|
|
||||||
organisationCreate,
|
organisationCreate,
|
||||||
organisationById,
|
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 [newStatusColour, setNewStatusColour] = useState(DEFAULT_STATUS_COLOUR);
|
||||||
const [statusError, setStatusError] = useState<string | null>(null);
|
const [statusError, setStatusError] = useState<string | null>(null);
|
||||||
const [statusToRemove, setStatusToRemove] = useState<string | null>(null);
|
const [statusToRemove, setStatusToRemove] = useState<string | null>(null);
|
||||||
|
const [issuesUsingStatus, setIssuesUsingStatus] = useState<number>(0);
|
||||||
const [reassignToStatus, setReassignToStatus] = useState<string>("");
|
const [reassignToStatus, setReassignToStatus] = useState<string>("");
|
||||||
|
|
||||||
const [confirmDialog, setConfirmDialog] = useState<{
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
@@ -218,11 +219,34 @@ function OrganisationsDialog({
|
|||||||
setStatusError(null);
|
setStatusError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveStatusClick = (status: string) => {
|
const handleRemoveStatusClick = async (status: string) => {
|
||||||
if (Object.keys(statuses).length <= 1) return;
|
if (Object.keys(statuses).length <= 1 || !selectedOrganisation) return;
|
||||||
setStatusToRemove(status);
|
try {
|
||||||
const remaining = Object.keys(statuses).filter((s) => s !== status);
|
await issue.statusCount({
|
||||||
setReassignToStatus(remaining[0] || "");
|
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") => {
|
const moveStatus = async (status: string, direction: "up" | "down") => {
|
||||||
@@ -276,7 +300,7 @@ function OrganisationsDialog({
|
|||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</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>
|
<DialogHeader>
|
||||||
<DialogTitle>Organisations</DialogTitle>
|
<DialogTitle>Organisations</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -471,7 +495,7 @@ function OrganisationsDialog({
|
|||||||
Object.keys(statuses).length <= 1
|
Object.keys(statuses).length <= 1
|
||||||
}
|
}
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
handleRemoveStatusClick(status)
|
void handleRemoveStatusClick(status)
|
||||||
}
|
}
|
||||||
className="hover:bg-destructive/10"
|
className="hover:bg-destructive/10"
|
||||||
>
|
>
|
||||||
@@ -587,7 +611,7 @@ function OrganisationsDialog({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Remove Status</DialogTitle>
|
<DialogTitle>Remove Status</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -596,7 +620,8 @@ function OrganisationsDialog({
|
|||||||
{statusToRemove ? (
|
{statusToRemove ? (
|
||||||
<StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />
|
<StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />
|
||||||
) : null}{" "}
|
) : 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>
|
</p>
|
||||||
<Select value={reassignToStatus} onValueChange={setReassignToStatus}>
|
<Select value={reassignToStatus} onValueChange={setReassignToStatus}>
|
||||||
<SelectTrigger className="w-min">
|
<SelectTrigger className="w-min">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_STATUS_COLOUR } from "@issue/shared";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const DARK_TEXT_COLOUR = "#0a0a0a";
|
const DARK_TEXT_COLOUR = "#0a0a0a";
|
||||||
@@ -14,7 +15,7 @@ const isLight = (hex: string): boolean => {
|
|||||||
|
|
||||||
export default function StatusTag({
|
export default function StatusTag({
|
||||||
status,
|
status,
|
||||||
colour,
|
colour = DEFAULT_STATUS_COLOUR,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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 { replaceStatus } from "@/lib/server/issue/replaceStatus";
|
export { replaceStatus } from "@/lib/server/issue/replaceStatus";
|
||||||
|
export { statusCount } from "@/lib/server/issue/statusCount";
|
||||||
export { update } from "@/lib/server/issue/update";
|
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