mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
timers should not be used on issues with multiple assignees
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -48,6 +48,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",
|
||||
@@ -318,6 +319,8 @@
|
||||
|
||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||
|
||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||
|
||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
@@ -800,6 +803,8 @@
|
||||
|
||||
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user