diff --git a/apps/web/app/office/components/new-task-dialog.test.tsx b/apps/web/app/office/components/new-task-dialog.test.tsx new file mode 100644 index 0000000000..973cf7df9b --- /dev/null +++ b/apps/web/app/office/components/new-task-dialog.test.tsx @@ -0,0 +1,72 @@ +import { describe, it, expect, afterEach, vi } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { TooltipProvider } from "@kandev/ui/tooltip"; + +import { CreateTaskButton } from "./new-task-dialog"; +import type { IssueDraft } from "./new-task-draft"; + +const BASE_DRAFT: IssueDraft = { + title: "", + description: "", + assigneeId: "", + projectId: "", + status: "todo", + priority: "medium", + showReviewer: false, + showApprover: false, + reviewerIds: [], + approverIds: [], +}; + +function renderButton(draft: Partial) { + return render( + + + , + ); +} + +const TEST_ID = "new-task-create-button"; + +function getButton() { + return screen.getByTestId(TEST_ID) as HTMLButtonElement; +} + +afterEach(() => cleanup()); + +describe("CreateTaskButton", () => { + it("disables the button and labels missing project when no project selected", () => { + renderButton({ title: "do thing" }); + expect(getButton().disabled).toBe(true); + // The tooltip content is rendered as accessible-name on the wrapper. + expect(screen.getByLabelText(/select a project to create a task/i)).toBeTruthy(); + }); + + it("disables the button and labels missing title when title empty", () => { + renderButton({ projectId: "proj-1" }); + expect(getButton().disabled).toBe(true); + expect(screen.getByLabelText(/add a title to create a task/i)).toBeTruthy(); + }); + + it("enables the button without a tooltip when both title and project are set", () => { + renderButton({ title: "do thing", projectId: "proj-1" }); + expect(getButton().disabled).toBe(false); + expect(screen.queryByLabelText(/create a task/i)).toBeNull(); + }); + + it("disables the button and shows 'Creating...' while submitting", () => { + render( + + + , + ); + const button = getButton(); + expect(button.disabled).toBe(true); + expect(button.textContent).toBe("Creating..."); + expect(screen.queryByLabelText(/create a task/i)).toBeNull(); + }); +}); diff --git a/apps/web/app/office/components/new-task-dialog.tsx b/apps/web/app/office/components/new-task-dialog.tsx index c273a5cf47..60a8ba5368 100644 --- a/apps/web/app/office/components/new-task-dialog.tsx +++ b/apps/web/app/office/components/new-task-dialog.tsx @@ -5,6 +5,7 @@ import { Button } from "@kandev/ui/button"; import { Badge } from "@kandev/ui/badge"; import { Textarea } from "@kandev/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle } from "@kandev/ui/dialog"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { toast } from "sonner"; import { useAppStore } from "@/components/state-provider"; import { createTask } from "@/lib/api/domains/kanban-api"; @@ -55,7 +56,7 @@ export function NewTaskDialog({ ); const handleCreate = useCallback(async () => { - if (!draft.title.trim() || !workspaceId) return; + if (!draft.title.trim() || !draft.projectId || !workspaceId) return; setSubmitting(true); try { const executionPolicy = buildExecutionPolicy(stages); @@ -168,14 +169,49 @@ function NewIssueDialogBody({ > Discard Draft - + ); } + +export function CreateTaskButton({ + draft, + submitting, + onCreate, +}: { + draft: IssueDraft; + submitting: boolean; + onCreate: () => void; +}) { + const missingTitle = !draft.title.trim(); + const missingProject = !draft.projectId; + const disabled = missingTitle || missingProject || submitting; + let reason: string | null = null; + if (missingProject) reason = "Select a project to create a task"; + else if (missingTitle) reason = "Add a title to create a task"; + + const button = ( + + ); + + if (!reason) return button; + + return ( + + + + {button} + + + {reason} + + ); +}