fix(web): remove mock data from dashboard telemetry/tasks/calendar (#656)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #656.
This commit is contained in:
@@ -4,61 +4,43 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Calendar as CalendarIcon, Clock, MapPin } from "lucide-react";
|
||||
import type { WidgetProps } from "@mosaic/shared";
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
title: string;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
location?: string;
|
||||
allDay: boolean;
|
||||
}
|
||||
import type { WidgetProps, Event } from "@mosaic/shared";
|
||||
import { fetchEvents } from "@/lib/api/events";
|
||||
|
||||
export function CalendarWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Mock data for now - will fetch from API later
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
let isMounted = true;
|
||||
|
||||
setTimeout(() => {
|
||||
setEvents([
|
||||
{
|
||||
id: "1",
|
||||
title: "Team Standup",
|
||||
startTime: new Date(today.setHours(9, 0, 0, 0)).toISOString(),
|
||||
endTime: new Date(today.setHours(9, 30, 0, 0)).toISOString(),
|
||||
location: "Zoom",
|
||||
allDay: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Project Review",
|
||||
startTime: new Date(today.setHours(14, 0, 0, 0)).toISOString(),
|
||||
endTime: new Date(today.setHours(15, 0, 0, 0)).toISOString(),
|
||||
location: "Conference Room A",
|
||||
allDay: false,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Sprint Planning",
|
||||
startTime: new Date(tomorrow.setHours(10, 0, 0, 0)).toISOString(),
|
||||
endTime: new Date(tomorrow.setHours(12, 0, 0, 0)).toISOString(),
|
||||
allDay: false,
|
||||
},
|
||||
]);
|
||||
setIsLoading(false);
|
||||
}, 500);
|
||||
const loadEvents = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchEvents();
|
||||
if (isMounted) {
|
||||
setEvents(data);
|
||||
}
|
||||
} catch {
|
||||
if (isMounted) {
|
||||
setEvents([]);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadEvents();
|
||||
|
||||
return (): void => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formatTime = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
const formatTime = (dateValue: Date | string): string => {
|
||||
const date = new Date(dateValue);
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
@@ -66,8 +48,8 @@ export function CalendarWidget({ id: _id, config: _config }: WidgetProps): React
|
||||
});
|
||||
};
|
||||
|
||||
const formatDay = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
const formatDay = (dateValue: Date | string): string => {
|
||||
const date = new Date(dateValue);
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
@@ -4,68 +4,56 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { CheckCircle, Circle, Clock, AlertCircle } from "lucide-react";
|
||||
import type { WidgetProps } from "@mosaic/shared";
|
||||
import { TaskPriority, TaskStatus, type WidgetProps, type Task } from "@mosaic/shared";
|
||||
import { fetchTasks } from "@/lib/api/tasks";
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export function TasksWidget({}: WidgetProps): React.JSX.Element {
|
||||
export function TasksWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Mock data for now - will fetch from API later
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setTasks([
|
||||
{
|
||||
id: "1",
|
||||
title: "Complete project documentation",
|
||||
status: "IN_PROGRESS",
|
||||
priority: "HIGH",
|
||||
dueDate: "2024-02-01",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Review pull requests",
|
||||
status: "NOT_STARTED",
|
||||
priority: "MEDIUM",
|
||||
dueDate: "2024-02-02",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Update dependencies",
|
||||
status: "COMPLETED",
|
||||
priority: "LOW",
|
||||
dueDate: "2024-01-30",
|
||||
},
|
||||
]);
|
||||
setIsLoading(false);
|
||||
}, 500);
|
||||
let isMounted = true;
|
||||
|
||||
const loadTasks = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchTasks();
|
||||
if (isMounted) {
|
||||
setTasks(data);
|
||||
}
|
||||
} catch {
|
||||
if (isMounted) {
|
||||
setTasks([]);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadTasks();
|
||||
|
||||
return (): void => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getPriorityIcon = (priority: string): React.JSX.Element => {
|
||||
const getPriorityIcon = (priority: TaskPriority): React.JSX.Element => {
|
||||
switch (priority) {
|
||||
case "HIGH":
|
||||
case TaskPriority.HIGH:
|
||||
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
||||
case "MEDIUM":
|
||||
case TaskPriority.MEDIUM:
|
||||
return <Clock className="w-4 h-4 text-yellow-500" />;
|
||||
case "LOW":
|
||||
case TaskPriority.LOW:
|
||||
return <Circle className="w-4 h-4 text-gray-400" />;
|
||||
default:
|
||||
return <Circle className="w-4 h-4 text-gray-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string): React.JSX.Element => {
|
||||
return status === "COMPLETED" ? (
|
||||
const getStatusIcon = (status: TaskStatus): React.JSX.Element => {
|
||||
return status === TaskStatus.COMPLETED ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Circle className="w-4 h-4 text-gray-400" />
|
||||
@@ -74,8 +62,8 @@ export function TasksWidget({}: WidgetProps): React.JSX.Element {
|
||||
|
||||
const stats = {
|
||||
total: tasks.length,
|
||||
inProgress: tasks.filter((t) => t.status === "IN_PROGRESS").length,
|
||||
completed: tasks.filter((t) => t.status === "COMPLETED").length,
|
||||
inProgress: tasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,
|
||||
completed: tasks.filter((t) => t.status === TaskStatus.COMPLETED).length,
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@@ -1,16 +1,58 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import type { Event } from "@mosaic/shared";
|
||||
import { CalendarWidget } from "../CalendarWidget";
|
||||
import { fetchEvents } from "@/lib/api/events";
|
||||
|
||||
vi.mock("@/lib/api/events", () => ({
|
||||
fetchEvents: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEvents: Event[] = [
|
||||
{
|
||||
id: "event-1",
|
||||
title: "API Planning",
|
||||
description: null,
|
||||
startTime: new Date("2026-02-01T09:00:00Z"),
|
||||
endTime: new Date("2026-02-01T09:30:00Z"),
|
||||
allDay: false,
|
||||
location: "Zoom",
|
||||
recurrence: null,
|
||||
creatorId: "user-1",
|
||||
workspaceId: "workspace-1",
|
||||
projectId: null,
|
||||
metadata: {},
|
||||
createdAt: new Date("2026-01-30T09:00:00Z"),
|
||||
updatedAt: new Date("2026-01-30T09:00:00Z"),
|
||||
},
|
||||
{
|
||||
id: "event-2",
|
||||
title: "API Review",
|
||||
description: null,
|
||||
startTime: new Date("2026-02-02T10:00:00Z"),
|
||||
endTime: new Date("2026-02-02T11:00:00Z"),
|
||||
allDay: false,
|
||||
location: "Room 1",
|
||||
recurrence: null,
|
||||
creatorId: "user-1",
|
||||
workspaceId: "workspace-1",
|
||||
projectId: null,
|
||||
metadata: {},
|
||||
createdAt: new Date("2026-01-30T09:00:00Z"),
|
||||
updatedAt: new Date("2026-01-30T09:00:00Z"),
|
||||
},
|
||||
];
|
||||
|
||||
async function finishWidgetLoad(): Promise<void> {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Loading events...")).not.toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
|
||||
describe("CalendarWidget", (): void => {
|
||||
beforeEach((): void => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fetchEvents).mockResolvedValue(mockEvents);
|
||||
vi.setSystemTime(new Date("2026-02-01T08:00:00Z"));
|
||||
});
|
||||
|
||||
@@ -24,15 +66,15 @@ describe("CalendarWidget", (): void => {
|
||||
expect(screen.getByText("Loading events...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders upcoming events after loading", async (): Promise<void> => {
|
||||
it("fetches and renders upcoming events after loading", async (): Promise<void> => {
|
||||
render(<CalendarWidget id="calendar-1" />);
|
||||
|
||||
await finishWidgetLoad();
|
||||
|
||||
expect(fetchEvents).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText("Upcoming Events")).toBeInTheDocument();
|
||||
expect(screen.getByText("Team Standup")).toBeInTheDocument();
|
||||
expect(screen.getByText("Project Review")).toBeInTheDocument();
|
||||
expect(screen.getByText("Sprint Planning")).toBeInTheDocument();
|
||||
expect(screen.getByText("API Planning")).toBeInTheDocument();
|
||||
expect(screen.getByText("API Review")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows relative day labels", async (): Promise<void> => {
|
||||
@@ -50,6 +92,15 @@ describe("CalendarWidget", (): void => {
|
||||
await finishWidgetLoad();
|
||||
|
||||
expect(screen.getByText("Zoom")).toBeInTheDocument();
|
||||
expect(screen.getByText("Conference Room A")).toBeInTheDocument();
|
||||
expect(screen.getByText("Room 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty state when no events are returned", async (): Promise<void> => {
|
||||
vi.mocked(fetchEvents).mockResolvedValueOnce([]);
|
||||
|
||||
render(<CalendarWidget id="calendar-1" />);
|
||||
await finishWidgetLoad();
|
||||
|
||||
expect(screen.getByText("No upcoming events")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,80 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { TaskStatus, TaskPriority, type Task } from "@mosaic/shared";
|
||||
import { TasksWidget } from "../TasksWidget";
|
||||
import { fetchTasks } from "@/lib/api/tasks";
|
||||
|
||||
vi.mock("@/lib/api/tasks", () => ({
|
||||
fetchTasks: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockTasks: Task[] = [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "API task one",
|
||||
description: null,
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.HIGH,
|
||||
dueDate: new Date("2026-02-03T09:00:00Z"),
|
||||
creatorId: "user-1",
|
||||
assigneeId: "user-1",
|
||||
workspaceId: "workspace-1",
|
||||
projectId: null,
|
||||
parentId: null,
|
||||
sortOrder: 0,
|
||||
metadata: {},
|
||||
completedAt: null,
|
||||
createdAt: new Date("2026-02-01T09:00:00Z"),
|
||||
updatedAt: new Date("2026-02-01T09:00:00Z"),
|
||||
},
|
||||
{
|
||||
id: "task-2",
|
||||
title: "API task two",
|
||||
description: null,
|
||||
status: TaskStatus.NOT_STARTED,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
dueDate: new Date("2026-02-04T09:00:00Z"),
|
||||
creatorId: "user-1",
|
||||
assigneeId: "user-1",
|
||||
workspaceId: "workspace-1",
|
||||
projectId: null,
|
||||
parentId: null,
|
||||
sortOrder: 1,
|
||||
metadata: {},
|
||||
completedAt: null,
|
||||
createdAt: new Date("2026-02-01T09:00:00Z"),
|
||||
updatedAt: new Date("2026-02-01T09:00:00Z"),
|
||||
},
|
||||
{
|
||||
id: "task-3",
|
||||
title: "API task three",
|
||||
description: null,
|
||||
status: TaskStatus.COMPLETED,
|
||||
priority: TaskPriority.LOW,
|
||||
dueDate: new Date("2026-02-05T09:00:00Z"),
|
||||
creatorId: "user-1",
|
||||
assigneeId: "user-1",
|
||||
workspaceId: "workspace-1",
|
||||
projectId: null,
|
||||
parentId: null,
|
||||
sortOrder: 2,
|
||||
metadata: {},
|
||||
completedAt: new Date("2026-02-02T09:00:00Z"),
|
||||
createdAt: new Date("2026-02-01T09:00:00Z"),
|
||||
updatedAt: new Date("2026-02-02T09:00:00Z"),
|
||||
},
|
||||
];
|
||||
|
||||
async function finishWidgetLoad(): Promise<void> {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Loading tasks...")).not.toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
|
||||
describe("TasksWidget", (): void => {
|
||||
beforeEach((): void => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach((): void => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fetchTasks).mockResolvedValue(mockTasks);
|
||||
});
|
||||
|
||||
it("renders loading state initially", (): void => {
|
||||
@@ -23,25 +83,26 @@ describe("TasksWidget", (): void => {
|
||||
expect(screen.getByText("Loading tasks...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders default summary stats", async (): Promise<void> => {
|
||||
it("fetches tasks and renders summary stats", async (): Promise<void> => {
|
||||
render(<TasksWidget id="tasks-1" />);
|
||||
|
||||
await finishWidgetLoad();
|
||||
|
||||
expect(fetchTasks).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText("Total")).toBeInTheDocument();
|
||||
expect(screen.getByText("In Progress")).toBeInTheDocument();
|
||||
expect(screen.getByText("Done")).toBeInTheDocument();
|
||||
expect(screen.getByText("3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders default task rows", async (): Promise<void> => {
|
||||
it("renders task rows from API response", async (): Promise<void> => {
|
||||
render(<TasksWidget id="tasks-1" />);
|
||||
|
||||
await finishWidgetLoad();
|
||||
|
||||
expect(screen.getByText("Complete project documentation")).toBeInTheDocument();
|
||||
expect(screen.getByText("Review pull requests")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update dependencies")).toBeInTheDocument();
|
||||
expect(screen.getByText("API task one")).toBeInTheDocument();
|
||||
expect(screen.getByText("API task two")).toBeInTheDocument();
|
||||
expect(screen.getByText("API task three")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows due date labels for each task", async (): Promise<void> => {
|
||||
@@ -51,4 +112,13 @@ describe("TasksWidget", (): void => {
|
||||
|
||||
expect(screen.getAllByText(/Due:/).length).toBe(3);
|
||||
});
|
||||
|
||||
it("shows empty state when API returns no tasks", async (): Promise<void> => {
|
||||
vi.mocked(fetchTasks).mockResolvedValueOnce([]);
|
||||
|
||||
render(<TasksWidget id="tasks-1" />);
|
||||
await finishWidgetLoad();
|
||||
|
||||
expect(screen.getByText("No tasks yet")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user