feat(FN-4224): tighten github tracking header to single-row layout in task
Restyles the GitHub tracking header in TaskDetailModal to a compact single-row layout with tighter spacing, adds test coverage for the header layout, and includes a regression verification pass alongside a changeset documenting the change. Fusion-Task-Id: FN-4224
This commit is contained in:
5
.changeset/FN-4185-open-new-room.md
Normal file
5
.changeset/FN-4185-open-new-room.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Auto-open newly created chat rooms in the dashboard so successful room creation immediately reveals the new thread, including collapsing the mobile sidebar.
|
||||
5
.changeset/fn-4224-github-tracking-header-compact.md
Normal file
5
.changeset/fn-4224-github-tracking-header-compact.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Tighten the Task Detail modal GitHub tracking header into a compact single-row summary with the inline Enable action and disclosure toggle staying aligned across desktop and mobile layouts.
|
||||
@@ -2867,7 +2867,13 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
||||
existingRoomNames={rooms.rooms.map((room) => room.name)}
|
||||
onCreate={async (draft) => {
|
||||
await rooms.createRoom({ name: draft.name, memberAgentIds: draft.memberAgentIds });
|
||||
if (chatScope !== "rooms") {
|
||||
setChatScope("rooms");
|
||||
}
|
||||
setCreateRoomOpen(false);
|
||||
if (isMobile) {
|
||||
setSidebarVisible(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -439,6 +439,7 @@
|
||||
.detail-github-tracking-enable {
|
||||
margin-left: auto;
|
||||
align-self: flex-start;
|
||||
flex: 0 0 auto;
|
||||
padding-block: calc(var(--space-xs) / 2);
|
||||
padding-inline: var(--space-sm);
|
||||
line-height: 1.2;
|
||||
@@ -488,12 +489,38 @@
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
}
|
||||
|
||||
.detail-github-tracking-section .detail-source-header {
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-github-tracking-section .detail-source-summary {
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-github-tracking-section .detail-source-empty {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-github-tracking-section .detail-source-toggle {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.detail-github-tracking-section .detail-github-tracking-enable {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.detail-github-tracking-content {
|
||||
margin-top: var(--space-sm);
|
||||
padding-top: var(--space-sm);
|
||||
margin-top: var(--space-xs);
|
||||
padding-top: var(--space-xs);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -1599,17 +1626,23 @@
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.detail-github-tracking-section .detail-source-summary {
|
||||
flex: 1 1 auto;
|
||||
.detail-github-tracking-section .detail-source-header {
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-github-tracking-enable {
|
||||
.detail-github-tracking-section .detail-source-summary {
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-github-tracking-section .detail-github-tracking-enable {
|
||||
margin-left: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-source-header .detail-source-toggle {
|
||||
.detail-github-tracking-section .detail-source-header .detail-source-toggle {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { useState } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { ChatView } from "../ChatView";
|
||||
@@ -181,6 +182,50 @@ function setupMockRooms(overrides: Partial<UseChatRoomsResult> = {}) {
|
||||
mockUseChatRooms.mockReturnValue(state);
|
||||
}
|
||||
|
||||
function createRoomFixture(name: string) {
|
||||
return {
|
||||
id: `room-${name}`,
|
||||
projectId: "proj-123",
|
||||
slug: name,
|
||||
name,
|
||||
createdAt: "2026-05-12T00:00:00.000Z",
|
||||
updatedAt: "2026-05-12T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function setupStatefulCreateRoomMock(options?: { createRejects?: boolean }) {
|
||||
const createRoom = vi.fn();
|
||||
|
||||
mockUseChatRooms.mockImplementation(() => {
|
||||
const [roomsState, setRoomsState] = useState<UseChatRoomsResult["rooms"]>([]);
|
||||
const [activeRoom, setActiveRoom] = useState<UseChatRoomsResult["activeRoom"]>(null);
|
||||
|
||||
return {
|
||||
...defaultRoomsState,
|
||||
rooms: roomsState,
|
||||
activeRoom,
|
||||
activeRoomMembers: activeRoom
|
||||
? [{ roomId: activeRoom.id, agentId: "agent-001", role: "member", addedAt: "2026-05-12T00:00:00.000Z" }]
|
||||
: [],
|
||||
createRoom: async ({ name, memberAgentIds }) => {
|
||||
createRoom({ name, memberAgentIds });
|
||||
if (options?.createRejects) {
|
||||
throw new Error("Failed to create room.");
|
||||
}
|
||||
const nextRoom = createRoomFixture(name);
|
||||
setRoomsState((previous) => [...previous, nextRoom]);
|
||||
setActiveRoom(nextRoom);
|
||||
return nextRoom;
|
||||
},
|
||||
selectRoom: (roomId) => {
|
||||
setActiveRoom(roomsState.find((room) => room.id === roomId) ?? null);
|
||||
},
|
||||
} satisfies UseChatRoomsResult;
|
||||
});
|
||||
|
||||
return { createRoom };
|
||||
}
|
||||
|
||||
function ensureMatchMedia() {
|
||||
if (!window.matchMedia) {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
@@ -208,7 +253,10 @@ function mockViewportMode(mode: "mobile" | "desktop") {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
_resetInitialViewportHeight();
|
||||
setupMockRooms();
|
||||
mockViewportMode("desktop");
|
||||
mockFetchDiscoveredSkills.mockResolvedValue([]);
|
||||
mockCreateObjectURL.mockImplementation((file: File) => `blob:${file.name}`);
|
||||
Object.defineProperty(URL, "createObjectURL", { value: mockCreateObjectURL, writable: true });
|
||||
@@ -222,7 +270,8 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.removeItem("fusion:chat-scope");
|
||||
localStorage.clear();
|
||||
_resetInitialViewportHeight();
|
||||
});
|
||||
|
||||
describe("ChatView", () => {
|
||||
@@ -2258,6 +2307,7 @@ describe("ChatView", () => {
|
||||
const toggle = screen.getByTestId("chat-thread-render-toggle");
|
||||
const providerIcon = identity.querySelector(".provider-icon");
|
||||
const modelTag = identity.querySelector(".chat-model-tag");
|
||||
const newChatButton = screen.getByTestId("chat-thread-new-chat-btn");
|
||||
|
||||
expect(header).toBeInTheDocument();
|
||||
expect(providerIcon).toBeInTheDocument();
|
||||
@@ -2265,7 +2315,8 @@ describe("ChatView", () => {
|
||||
expect(modelTag).toBeInTheDocument();
|
||||
expect(modelTag).toHaveTextContent("Claude Sonnet 4.5");
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(header?.children[header.children.length - 1]).toBe(toggle);
|
||||
expect(header?.children[header.children.length - 2]).toBe(toggle);
|
||||
expect(header?.children[header.children.length - 1]).toBe(newChatButton);
|
||||
expect(document.querySelectorAll(".chat-thread-header .chat-model-tag")).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -2794,6 +2845,82 @@ describe("ChatView sidebar structure", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("room creation", () => {
|
||||
it("opens the newly created room and collapses the mobile sidebar on success", async () => {
|
||||
const viewportSpy = mockViewportMode("mobile");
|
||||
const { createRoom } = setupStatefulCreateRoomMock();
|
||||
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||
localStorage.setItem("fusion:chat-scope", "rooms");
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("chat-create-room-btn"));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Create room" });
|
||||
await userEvent.type(within(dialog).getByLabelText("Room name"), "newroom");
|
||||
await userEvent.click(within(screen.getByTestId("create-room-member-list")).getByText("Alpha"));
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: "Create room" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createRoom).toHaveBeenCalledWith({ name: "newroom", memberAgentIds: ["agent-001"] });
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".chat-sidebar")).toHaveClass("chat-sidebar--hidden");
|
||||
});
|
||||
expect(screen.queryByRole("dialog", { name: "Create room" })).toBeNull();
|
||||
expect(within(document.querySelector(".chat-room-thread-header") as HTMLElement).getByText("#newroom")).toBeInTheDocument();
|
||||
|
||||
viewportSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("opens the newly created room on desktop without hiding the sidebar", async () => {
|
||||
const viewportSpy = mockViewportMode("desktop");
|
||||
const { createRoom } = setupStatefulCreateRoomMock();
|
||||
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||
localStorage.setItem("fusion:chat-scope", "rooms");
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("chat-create-room-btn"));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Create room" });
|
||||
await userEvent.type(within(dialog).getByLabelText("Room name"), "newroom");
|
||||
await userEvent.click(within(screen.getByTestId("create-room-member-list")).getByText("Alpha"));
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: "Create room" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createRoom).toHaveBeenCalledWith({ name: "newroom", memberAgentIds: ["agent-001"] });
|
||||
});
|
||||
expect(document.querySelector(".chat-sidebar")).not.toHaveClass("chat-sidebar--hidden");
|
||||
expect(screen.queryByRole("dialog", { name: "Create room" })).toBeNull();
|
||||
expect(within(document.querySelector(".chat-room-thread-header") as HTMLElement).getByText("#newroom")).toBeInTheDocument();
|
||||
|
||||
viewportSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("keeps the modal open and sidebar visible when room creation fails", async () => {
|
||||
const viewportSpy = mockViewportMode("mobile");
|
||||
const { createRoom } = setupStatefulCreateRoomMock({ createRejects: true });
|
||||
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||
localStorage.setItem("fusion:chat-scope", "rooms");
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("chat-create-room-btn"));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Create room" });
|
||||
await userEvent.type(within(dialog).getByLabelText("Room name"), "newroom");
|
||||
await userEvent.click(within(screen.getByTestId("create-room-member-list")).getByText("Alpha"));
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: "Create room" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createRoom).toHaveBeenCalledWith({ name: "newroom", memberAgentIds: ["agent-001"] });
|
||||
});
|
||||
expect(screen.getByRole("dialog", { name: "Create room" })).toBeInTheDocument();
|
||||
expect(document.querySelector(".chat-sidebar")).not.toHaveClass("chat-sidebar--hidden");
|
||||
expect(screen.queryByText("#newroom")).toBeNull();
|
||||
|
||||
viewportSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Direct/Rooms scope toggle", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { loadAllAppCss } from "../../test/cssFixture";
|
||||
import { TaskDetailModal } from "../TaskDetailModal";
|
||||
import { makeTask, noop, noopDelete, noopMerge, noopMove, noopOpenDetail, setupTaskDetailModalHooks } from "./TaskDetailModal.test-helpers";
|
||||
|
||||
setupTaskDetailModalHooks();
|
||||
|
||||
describe("FN-4224 GitHub tracking header layout", () => {
|
||||
it("keeps the summary, enable action, and disclosure toggle on one row across desktop and mobile CSS", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
column: "todo",
|
||||
githubTracking: { enabled: false },
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("GitHub tracking")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tracking is currently disabled")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Enable GitHub tracking" })).toHaveTextContent("Enable");
|
||||
expect(screen.getByRole("button", { name: "Expand GitHub tracking details" })).toBeInTheDocument();
|
||||
|
||||
const css = loadAllAppCss();
|
||||
|
||||
expect(css).toMatch(
|
||||
/\.detail-github-tracking-section\s+\.detail-source-header\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*align-items:\s*center;[^}]*min-width:\s*0;/,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/\.detail-github-tracking-section\s+\.detail-source-summary\s*\{[^}]*flex:\s*1 1 auto;[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;/,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.detail-github-tracking-section\s+\.detail-source-header\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;[^}]*\}[\s\S]*?\.detail-github-tracking-section\s+\.detail-source-summary\s*\{[^}]*flex:\s*1 1 auto;[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;[^}]*\}/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ const qualityAppTests = [
|
||||
"app/api/**/*.test.ts",
|
||||
// Representative workflow/component coverage. Exhaustive modal/view suites
|
||||
// stay available in the full `dashboard-app` project.
|
||||
"app/components/__tests__/{ActiveAgentsPanel,AgentMentionPopup,AgentMetricsBar,AgentReflectionsTab,AgentTokenStatsPanel,AuthTokenRecoveryDialog,Board,board-mobile-view-switch,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DirectoryPicker,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MobileNavBar,NewTaskModal,NodeCard,NodeHealthDot,NodeStatusIndicator,ProjectCard,ProjectSelector,ProviderIcon,QuickChatFAB,ResearchView,StashRecoveryView,TaskCard,TaskChangesTab,TaskComments,TaskDocumentsTab,TaskForm,ThemeSelectorSwatchContract,WorkflowResultsTab}.test.tsx",
|
||||
"app/components/__tests__/{ActiveAgentsPanel,AgentMentionPopup,AgentMetricsBar,AgentReflectionsTab,AgentTokenStatsPanel,AuthTokenRecoveryDialog,Board,board-mobile-view-switch,ChatView,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DirectoryPicker,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MobileNavBar,NewTaskModal,NodeCard,NodeHealthDot,NodeStatusIndicator,ProjectCard,ProjectSelector,ProviderIcon,QuickChatFAB,ResearchView,StashRecoveryView,TaskCard,TaskChangesTab,TaskComments,TaskDetailModal.github-tracking-header,TaskDocumentsTab,TaskForm,ThemeSelectorSwatchContract,WorkflowResultsTab}.test.tsx",
|
||||
// Hooks and utilities are fast, user-visible state/formatting behavior.
|
||||
"app/context/**/*.test.tsx",
|
||||
"app/hooks/__tests__/{useAgents,useAgentLogs,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodeSettingsSync,useProjects,useQuickChat,useTasks,useTerminalSessions,useTheme,useToast,useUsageData,useViewState}.test.{ts,tsx}",
|
||||
|
||||
Reference in New Issue
Block a user