diff --git a/apps/web/src/app/(authenticated)/files/page.tsx b/apps/web/src/app/(authenticated)/files/page.tsx
index ec31624..75dae38 100644
--- a/apps/web/src/app/(authenticated)/files/page.tsx
+++ b/apps/web/src/app/(authenticated)/files/page.tsx
@@ -26,11 +26,7 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { fetchEntries, createEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge";
-import type {
- EntriesResponse,
- CreateEntryData,
- EntryFilters,
-} from "@/lib/api/knowledge";
+import type { EntriesResponse, CreateEntryData, EntryFilters } from "@/lib/api/knowledge";
/* ---------------------------------------------------------------------------
Helpers
@@ -439,7 +435,7 @@ function CreateEntryDialog({
.then((tags) => {
setAvailableTags(tags);
})
- .catch((err) => {
+ .catch((err: unknown) => {
console.error("Failed to load tags:", err);
});
}
@@ -730,7 +726,9 @@ function CreateEntryDialog({
}}
onBlur={() => {
// Delay to allow click on suggestion
- setTimeout(() => setShowSuggestions(false), 150);
+ setTimeout(() => {
+ setShowSuggestions(false);
+ }, 150);
}}
onFocus={() => {
if (tagInput.length > 0) setShowSuggestions(true);
diff --git a/apps/web/src/app/(authenticated)/kanban/page.test.tsx b/apps/web/src/app/(authenticated)/kanban/page.test.tsx
new file mode 100644
index 0000000..9b98cc3
--- /dev/null
+++ b/apps/web/src/app/(authenticated)/kanban/page.test.tsx
@@ -0,0 +1,188 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { Task } from "@mosaic/shared";
+import { TaskPriority, TaskStatus } from "@mosaic/shared";
+import KanbanPage from "./page";
+
+const mockReplace = vi.fn();
+let mockSearchParams = new URLSearchParams();
+
+vi.mock("next/navigation", () => ({
+ useRouter: (): { replace: typeof mockReplace } => ({ replace: mockReplace }),
+ useSearchParams: (): URLSearchParams => mockSearchParams,
+}));
+
+vi.mock("@hello-pangea/dnd", () => ({
+ DragDropContext: ({ children }: { children: React.ReactNode }): React.JSX.Element => (
+
{children}
+ ),
+ Droppable: ({
+ children,
+ droppableId,
+ }: {
+ children: (provided: {
+ innerRef: (el: HTMLElement | null) => void;
+ droppableProps: Record;
+ placeholder: React.ReactNode;
+ }) => React.ReactNode;
+ droppableId: string;
+ }): React.JSX.Element => (
+
+ {children({
+ innerRef: () => {
+ /* noop */
+ },
+ droppableProps: {},
+ placeholder: null,
+ })}
+
+ ),
+ Draggable: ({
+ children,
+ draggableId,
+ }: {
+ children: (
+ provided: {
+ innerRef: (el: HTMLElement | null) => void;
+ draggableProps: { style: Record };
+ dragHandleProps: Record;
+ },
+ snapshot: { isDragging: boolean }
+ ) => React.ReactNode;
+ draggableId: string;
+ index: number;
+ }): React.JSX.Element => (
+
+ {children(
+ {
+ innerRef: () => {
+ /* noop */
+ },
+ draggableProps: { style: {} },
+ dragHandleProps: {},
+ },
+ { isDragging: false }
+ )}
+
+ ),
+}));
+
+vi.mock("@/components/ui/MosaicSpinner", () => ({
+ MosaicSpinner: ({ label }: { label?: string }): React.JSX.Element => (
+ {label ?? "Loading..."}
+ ),
+}));
+
+const mockUseWorkspaceId = vi.fn<() => string | null>();
+vi.mock("@/lib/hooks", () => ({
+ useWorkspaceId: (): string | null => mockUseWorkspaceId(),
+}));
+
+const mockFetchTasks = vi.fn<() => Promise>();
+const mockUpdateTask = vi.fn<() => Promise>();
+const mockCreateTask = vi.fn<() => Promise>();
+vi.mock("@/lib/api/tasks", () => ({
+ fetchTasks: (...args: unknown[]): Promise => mockFetchTasks(...(args as [])),
+ updateTask: (...args: unknown[]): Promise => mockUpdateTask(...(args as [])),
+ createTask: (...args: unknown[]): Promise => mockCreateTask(...(args as [])),
+}));
+
+const mockFetchProjects = vi.fn<() => Promise>();
+vi.mock("@/lib/api/projects", () => ({
+ fetchProjects: (...args: unknown[]): Promise => mockFetchProjects(...(args as [])),
+}));
+
+const createdTask: Task = {
+ id: "task-new-1",
+ title: "Ship Kanban add task flow",
+ description: null,
+ status: TaskStatus.NOT_STARTED,
+ priority: TaskPriority.MEDIUM,
+ dueDate: null,
+ creatorId: "user-1",
+ assigneeId: null,
+ workspaceId: "ws-1",
+ projectId: "project-42",
+ parentId: null,
+ sortOrder: 0,
+ metadata: {},
+ completedAt: null,
+ createdAt: new Date("2026-03-01"),
+ updatedAt: new Date("2026-03-01"),
+};
+
+describe("KanbanPage add task flow", (): void => {
+ beforeEach((): void => {
+ vi.clearAllMocks();
+ mockSearchParams = new URLSearchParams("project=project-42");
+ mockUseWorkspaceId.mockReturnValue("ws-1");
+ mockFetchTasks.mockResolvedValue([]);
+ mockFetchProjects.mockResolvedValue([]);
+ mockCreateTask.mockResolvedValue(createdTask);
+ });
+
+ it("opens add-task form in a column and creates a task via API", async (): Promise => {
+ const user = userEvent.setup();
+
+ render();
+
+ await waitFor((): void => {
+ expect(screen.getByText("Kanban Board")).toBeInTheDocument();
+ });
+
+ // Click the "+ Add task" button in the To Do column
+ const addTaskButtons = screen.getAllByRole("button", { name: /\+ Add task/i });
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ await user.click(addTaskButtons[0]!); // First column is "To Do"
+
+ // Type in the title input
+ const titleInput = screen.getByPlaceholderText("Task title...");
+ await user.type(titleInput, createdTask.title);
+
+ // Click the Add button
+ await user.click(screen.getByRole("button", { name: /✓ Add/i }));
+
+ await waitFor((): void => {
+ expect(mockCreateTask).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: createdTask.title,
+ status: TaskStatus.NOT_STARTED,
+ projectId: "project-42",
+ }),
+ "ws-1"
+ );
+ });
+ });
+
+ it("cancels add-task form when pressing Escape", async (): Promise => {
+ const user = userEvent.setup();
+
+ render();
+
+ await waitFor((): void => {
+ expect(screen.getByText("Kanban Board")).toBeInTheDocument();
+ });
+
+ // Click the "+ Add task" button
+ const addTaskButtons = screen.getAllByRole("button", { name: /\+ Add task/i });
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ await user.click(addTaskButtons[0]!);
+
+ // Type in the title input
+ const titleInput = screen.getByPlaceholderText("Task title...");
+ await user.type(titleInput, "Test task");
+
+ // Press Escape to cancel
+ await user.keyboard("{Escape}");
+
+ // Form should be closed, back to "+ Add task" button
+ await waitFor((): void => {
+ const buttons = screen.getAllByRole("button", { name: /\+ Add task/i });
+ expect(buttons.length).toBe(5); // One per column
+ });
+
+ // Should not have called createTask
+ expect(mockCreateTask).not.toHaveBeenCalled();
+ });
+});