timers should not be used on issues with multiple assignees

This commit is contained in:
Oliver Bryan
2026-01-20 23:13:12 +00:00
parent 6a91b4b9f3
commit 42a537a967
6 changed files with 53 additions and 10 deletions

View File

@@ -182,3 +182,11 @@ export async function getIssuesWithUsersByProject(projectId: number): Promise<Is
Assignees: assigneesByIssue.get(row.Issue.id) || [],
}));
}
export async function getIssueAssigneeCount(issueId: number): Promise<number> {
const [result] = await db
.select({ count: sql<number>`COUNT(*)` })
.from(IssueAssignee)
.where(eq(IssueAssignee.issueId, issueId));
return result?.count ?? 0;
}

View File

@@ -5,7 +5,12 @@ import {
TimerToggleRequestSchema,
} from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import { appendTimestamp, createTimedSession, getActiveTimedSession } from "../../db/queries";
import {
appendTimestamp,
createTimedSession,
getActiveTimedSession,
getIssueAssigneeCount,
} from "../../db/queries";
import { parseJsonBody } from "../../validation";
export default async function timerToggle(req: AuthedRequest) {
@@ -14,6 +19,14 @@ export default async function timerToggle(req: AuthedRequest) {
const { issueId } = parsed.data;
const assigneeCount = await getIssueAssigneeCount(issueId);
if (assigneeCount > 1) {
return Response.json(
{ error: "Timers cannot be used on issues with multiple assignees", code: "MULTIPLE_ASSIGNEES" },
{ status: 400 },
);
}
const activeSession = await getActiveTimedSession(req.userId, issueId);
if (!activeSession) {

View File

@@ -20,6 +20,7 @@
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@sprint/shared": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.19",

View File

@@ -69,6 +69,10 @@ export function IssueDetailPane() {
const [isSavingDescription, setIsSavingDescription] = useState(false);
const descriptionRef = useRef<HTMLTextAreaElement>(null);
const isAssignee = assigneeIds.some((id) => user?.id === Number(id));
const actualAssigneeIds = assigneeIds.filter((id) => id !== "unassigned");
const hasMultipleAssignees = actualAssigneeIds.length > 1;
useEffect(() => {
if (!issueData) return;
setSprintId(issueData.Issue.sprintId?.toString() ?? "unassigned");
@@ -421,12 +425,20 @@ export function IssueDetailPane() {
<SmallUserDisplay user={issueData.Creator} className={"text-sm"} />
</div>
<div className="flex items-center gap-2">
{assigneeIds.some((id) => user?.id === Number(id)) && (
<TimerModal issueId={issueData.Issue.id} />
)}
<TimerDisplay issueId={issueData.Issue.id} />
</div>
{isAssignee && (
<div className={cn("flex flex-col gap-2", hasMultipleAssignees && "cursor-not-allowed")}>
<div className="flex items-center gap-2">
<TimerModal issueId={issueData.Issue.id} disabled={hasMultipleAssignees} />
<TimerDisplay issueId={issueData.Issue.id} />
</div>
{hasMultipleAssignees && (
<span className="text-xs text-destructive/85 font-600">
Timers cannot be used on issues with multiple assignees
</span>
)}
</div>
)}
<ConfirmDialog
open={deleteOpen}
onOpenChange={setDeleteOpen}

View File

@@ -4,13 +4,17 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import Icon from "@/components/ui/icon";
export function TimerModal({ issueId }: { issueId: number }) {
export function TimerModal({ issueId, disabled }: { issueId: number; disabled?: boolean }) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<DialogTrigger asChild disabled={disabled}>
<Button
variant="outline"
size="sm"
disabled={disabled}
>
<Icon icon="timer" className="size-4" />
Timer
</Button>