fix(FN-820): fix activity log data source selection in project view
- Fix ActivityLogModal to correctly select between project and central activity logs based on project context - Add regression tests verifying data source selection logic for single-project and multi-project scenarios - Update README documentation with activity log data source rules and selection behavior
This commit is contained in:
@@ -224,8 +224,8 @@ Browse and edit task worktree files directly from the task detail modal:
|
||||
View a centralized timeline of all task lifecycle events. Click the history icon in the header to open the Activity Log modal.
|
||||
|
||||
**Data Source**:
|
||||
- **Single-project mode** (default): Reads from the per-project activity log via `/api/activity`, which is always populated with task lifecycle events for the current project.
|
||||
- **Multi-project mode**: When projects are registered, the modal reads from the unified central feed via `/api/activity-feed`, which aggregates activity across all registered projects. A project filter dropdown allows narrowing results to a specific project.
|
||||
- **Project view** (when a project is selected): Reads from the per-project activity log via `/api/activity`, which is always populated with task lifecycle events for the current project. This ensures reliable activity visibility in normal dashboard use.
|
||||
- **Overview mode** (no project selected): Reads from the unified central feed via `/api/activity-feed`, which aggregates activity across all registered projects. The project filter dropdown allows narrowing results to a specific project.
|
||||
|
||||
**Features**:
|
||||
- **Event Types**: Track task:created, task:moved, task:merged, task:failed, task:deleted, and settings:updated events
|
||||
|
||||
@@ -732,6 +732,7 @@ function AppInner() {
|
||||
tasks={tasks}
|
||||
projectId={currentProject?.id}
|
||||
projects={projects}
|
||||
currentProject={currentProject}
|
||||
onOpenTaskDetail={(taskId) => {
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
if (task) {
|
||||
|
||||
@@ -15,6 +15,8 @@ interface ActivityLogModalProps {
|
||||
projects?: ProjectInfo[];
|
||||
/** Called when project filter changes */
|
||||
onProjectFilterChange?: (projectId: string | undefined) => void;
|
||||
/** Current project context - when set, uses per-project activity log */
|
||||
currentProject?: ProjectInfo | null;
|
||||
}
|
||||
|
||||
const EVENT_TYPE_LABELS: Record<ActivityEventType, string> = {
|
||||
@@ -57,10 +59,11 @@ function formatTimestamp(timestamp: string): string {
|
||||
* ActivityLogModal - Activity log with project attribution and filtering
|
||||
*
|
||||
* Data source selection:
|
||||
* - Single-project mode (no projects list): reads from the per-project activity
|
||||
* - Project view (currentProject set): reads from the per-project activity
|
||||
* log via /api/activity, which is always populated with task lifecycle events.
|
||||
* - Multi-project mode (projects list provided): reads from the unified central
|
||||
* - Overview mode (no currentProject): reads from the unified central
|
||||
* feed via /api/activity-feed, which aggregates activity across all projects.
|
||||
* The project filter dropdown allows narrowing results to a specific project.
|
||||
*
|
||||
* Features:
|
||||
* - Project name badge for each activity entry
|
||||
@@ -76,6 +79,7 @@ export function ActivityLogModal({
|
||||
projectId,
|
||||
projects = [],
|
||||
onProjectFilterChange,
|
||||
currentProject,
|
||||
}: ActivityLogModalProps) {
|
||||
const [filteredType, setFilteredType] = useState<ActivityEventType | "all">("all");
|
||||
const [filteredProjectId, setFilteredProjectId] = useState<string | "all">(projectId || "all");
|
||||
@@ -90,10 +94,14 @@ export function ActivityLogModal({
|
||||
const activityType = filteredType === "all" ? undefined : filteredType;
|
||||
const activeProjectId = filteredProjectId === "all" ? undefined : filteredProjectId;
|
||||
|
||||
// Determine data source: use unified central feed only when projects list
|
||||
// is provided (multi-project context). In single-project mode the hook reads
|
||||
// from the per-project activity log which is always populated.
|
||||
const useCentralFeed = projects.length > 0;
|
||||
// Determine data source:
|
||||
// - In project view (currentProject set): use per-project activity log (/api/activity)
|
||||
// which is always populated with task lifecycle events for the current project.
|
||||
// - In overview mode (no currentProject): use unified central feed (/api/activity-feed)
|
||||
// which aggregates activity across all registered projects.
|
||||
// The project filter dropdown (projects prop) still appears to filter by project,
|
||||
// but the default data source is the per-project log in project view.
|
||||
const useCentralFeed = !currentProject && projects.length > 0;
|
||||
|
||||
// Use the hook for data fetching
|
||||
const {
|
||||
|
||||
@@ -145,7 +145,7 @@ describe("ActivityLogModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("calls unified feed API when projects are provided (multi-project mode)", async () => {
|
||||
it("calls unified feed API when projects are provided but no currentProject (overview mode)", async () => {
|
||||
const mockProjects = [
|
||||
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
|
||||
];
|
||||
@@ -161,11 +161,102 @@ describe("ActivityLogModal", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Multi-project mode: uses fetchActivityFeed (not fetchActivityLog)
|
||||
// Overview mode (no currentProject): uses fetchActivityFeed
|
||||
expect(mockFetchActivityFeed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Regression Tests for FN-820 ────────────────────────────────────
|
||||
// The bug was that the modal used the unified central feed whenever projects
|
||||
// existed, even in normal project view where per-project activity log should be used.
|
||||
|
||||
it("uses per-project log when currentProject is set even with multiple projects registered", async () => {
|
||||
const mockProjects = [
|
||||
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
|
||||
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
|
||||
];
|
||||
const mockCurrentProject = { id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" };
|
||||
|
||||
render(
|
||||
<ActivityLogModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
tasks={mockTasks}
|
||||
projects={mockProjects}
|
||||
currentProject={mockCurrentProject}
|
||||
onOpenTaskDetail={mockOnOpenTaskDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Project view (currentProject set): uses per-project log even with multiple projects
|
||||
expect(mockFetchActivityLog).toHaveBeenCalled();
|
||||
expect(mockFetchActivityFeed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses per-project log when currentProject is set and projects list is empty", async () => {
|
||||
const mockCurrentProject = { id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" };
|
||||
|
||||
render(
|
||||
<ActivityLogModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
tasks={mockTasks}
|
||||
currentProject={mockCurrentProject}
|
||||
onOpenTaskDetail={mockOnOpenTaskDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Project view: uses per-project log
|
||||
expect(mockFetchActivityLog).toHaveBeenCalled();
|
||||
expect(mockFetchActivityFeed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses unified feed in overview mode with multiple projects but no currentProject", async () => {
|
||||
const mockProjects = [
|
||||
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
|
||||
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
|
||||
];
|
||||
|
||||
render(
|
||||
<ActivityLogModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
tasks={mockTasks}
|
||||
projects={mockProjects}
|
||||
// No currentProject - overview mode
|
||||
onOpenTaskDetail={mockOnOpenTaskDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Overview mode: uses unified feed
|
||||
expect(mockFetchActivityFeed).toHaveBeenCalled();
|
||||
expect(mockFetchActivityLog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses per-project log by default when only projectId is passed (backward compatible)", async () => {
|
||||
render(
|
||||
<ActivityLogModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
tasks={mockTasks}
|
||||
projectId="proj_1"
|
||||
onOpenTaskDetail={mockOnOpenTaskDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Default behavior without projects: uses per-project log
|
||||
expect(mockFetchActivityLog).toHaveBeenCalled();
|
||||
expect(mockFetchActivityFeed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters by type when dropdown changed", async () => {
|
||||
render(
|
||||
<ActivityLogModal
|
||||
|
||||
Reference in New Issue
Block a user