fix(dashboard): move header to overview
This commit is contained in:
@@ -2,35 +2,6 @@
|
||||
FNXC:BoardWorkflows 2026-06-20-09:02:
|
||||
The board needs a neutral first-paint shell whenever workflow lanes are enabled but metadata is not ready, so users never see the legacy single-lane board flash before workflow lanes load.
|
||||
*/
|
||||
.dashboard-board-view {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:DashboardHeader 2026-06-22-16:05:
|
||||
The Dashboard board now sits under the shared ViewHeader like Artifacts/Skills. The board remains the scroll container below the header, so reset the old full-height assumption within this shell and let flex allocate the remaining height.
|
||||
*/
|
||||
.dashboard-board-view > .board,
|
||||
.dashboard-board-view .board-workflow-view {
|
||||
flex: 1 1 auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dashboard-board-view__count {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.board.board-workflows-skeleton {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -16,9 +16,6 @@ import { WorkflowSwitcher } from "./WorkflowSwitcher";
|
||||
import { computeWorkflowStatusCounts } from "./workflowStatusCounts";
|
||||
import { writeBoardWorkflowsCache } from "../utils/boardWorkflowsCache";
|
||||
import { useBoardWorkflows } from "../hooks/useBoardWorkflows";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LayoutDashboard } from "lucide-react";
|
||||
import { ViewHeader } from "./ViewHeader";
|
||||
|
||||
interface BoardProps {
|
||||
tasks: Task[];
|
||||
@@ -149,7 +146,6 @@ function BoardWorkflowSkeleton({ empty = false }: { empty?: boolean }) {
|
||||
}
|
||||
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow, workflowColumnsEnabled, settingsLoaded, workflowControlsInHeader = false }: BoardProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
|
||||
const archivedLoadedRef = useRef(false);
|
||||
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
|
||||
@@ -519,29 +515,9 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
const shouldGateLegacyBoard = boardWorkflows === null
|
||||
? (workflowColumnsEnabled === true || settingsLoaded === false)
|
||||
: boardWorkflows.flagEnabled === true && boardWorkflows.workflows.length === 0;
|
||||
/*
|
||||
FNXC:DashboardHeader 2026-06-22-16:05:
|
||||
Dashboard/Board is a first-class left-sidebar view, so it needs the same canonical ViewHeader chrome as Artifacts, Skills, and Goals instead of letting the board columns touch the top app chrome. Keep the board itself as the scroll owner below the header so horizontal lane scrolling and mobile snap behavior are unchanged.
|
||||
*/
|
||||
const dashboardHeader = (
|
||||
<ViewHeader
|
||||
icon={LayoutDashboard}
|
||||
title={t("dashboard.title", "Dashboard")}
|
||||
actions={(
|
||||
<span className="dashboard-board-view__count">
|
||||
{t("dashboard.taskCount", "{{count}} task{{plural}}", { count: tasks.length, plural: tasks.length === 1 ? "" : "s" })}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (shouldGateLegacyBoard) {
|
||||
return (
|
||||
<div className="dashboard-board-view">
|
||||
{dashboardHeader}
|
||||
<BoardWorkflowSkeleton empty={boardWorkflows?.flagEnabled === true} />
|
||||
</div>
|
||||
);
|
||||
return <BoardWorkflowSkeleton empty={boardWorkflows?.flagEnabled === true} />;
|
||||
}
|
||||
|
||||
if (workflowMode && selectedWorkflow) {
|
||||
@@ -573,22 +549,20 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="dashboard-board-view">
|
||||
{dashboardHeader}
|
||||
<div className="board-workflow-view">
|
||||
{workflowControlsInHeader && headerWorkflowSlot ? relocatedWorkflowToolbar : workflowToolbar}
|
||||
<main
|
||||
className="board board-workflow-columns"
|
||||
id="board"
|
||||
ref={boardRef}
|
||||
onDragStart={(e) => {
|
||||
const id = (e.target as HTMLElement)?.closest?.("[data-id]")?.getAttribute("data-id");
|
||||
if (id) draggingTaskIdRef.current = id;
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
draggingTaskIdRef.current = null;
|
||||
}}
|
||||
>
|
||||
<div className="board-workflow-view">
|
||||
{workflowControlsInHeader && headerWorkflowSlot ? relocatedWorkflowToolbar : workflowToolbar}
|
||||
<main
|
||||
className="board board-workflow-columns"
|
||||
id="board"
|
||||
ref={boardRef}
|
||||
onDragStart={(e) => {
|
||||
const id = (e.target as HTMLElement)?.closest?.("[data-id]")?.getAttribute("data-id");
|
||||
if (id) draggingTaskIdRef.current = id;
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
draggingTaskIdRef.current = null;
|
||||
}}
|
||||
>
|
||||
{selectedWorkflowColumns.map((columnDef) => {
|
||||
const isCreateColumn = columnDef.id === selectedWorkflowCreateColumnId;
|
||||
return (
|
||||
@@ -683,15 +657,13 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
onToggleCollapse={handleToggleArchivedCollapse}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard-board-view">
|
||||
{dashboardHeader}
|
||||
<>
|
||||
<main className="board" id="board" ref={boardRef}>
|
||||
{COLUMNS.map((col) => (
|
||||
<Column
|
||||
@@ -734,6 +706,6 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
/>
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,8 +81,10 @@ Direct/Rooms now lives in the Chat ViewHeader immediately before New Chat. The c
|
||||
width: clamp(128px, 24vw, 220px);
|
||||
min-width: min(128px, 100%);
|
||||
max-width: 220px;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.chat-sidebar-scope-btn {
|
||||
@@ -101,6 +103,8 @@ Direct/Rooms now lives in the Chat ViewHeader immediately before New Chat. The c
|
||||
min-height: 0;
|
||||
height: var(--view-header-content-row, 28px);
|
||||
padding: 0 clamp(var(--space-xs), 1.2vw, var(--space-sm));
|
||||
border: 1px solid transparent;
|
||||
border-radius: calc(var(--radius-md) - 2px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -122,6 +126,10 @@ Direct/Rooms now lives in the Chat ViewHeader immediately before New Chat. The c
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chat-view-header-scope-toggle .chat-sidebar-scope-btn--active {
|
||||
border-color: var(--todo);
|
||||
}
|
||||
|
||||
.chat-sidebar-rooms {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
@@ -4,17 +4,31 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:DashboardHeader 2026-06-22-16:42:
|
||||
Dashboard overview uses the canonical ViewHeader at the top; the overview body owns padding, max-width, and vertical scrolling below that header so the header aligns with Artifacts/Skills while the project content keeps its readable width.
|
||||
*/
|
||||
.project-overview__body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
padding: var(--space-xl);
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: var(--space-xl);
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.project-overview--empty {
|
||||
.project-overview__body--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -124,6 +138,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
}
|
||||
|
||||
/* --- Filter Tabs --- */
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ProjectCard } from "./ProjectCard";
|
||||
import { getNodeMappingsForProject, resolveNodeDisplayName } from "../utils/nodeProjectAssignment";
|
||||
import { ProjectGridSkeleton } from "./ProjectGridSkeleton";
|
||||
import { useProjectHealth } from "../hooks/useProjectHealth";
|
||||
import { ViewHeader } from "./ViewHeader";
|
||||
|
||||
export interface ProjectOverviewProps {
|
||||
projects: ProjectInfoWithSource[];
|
||||
@@ -235,31 +236,60 @@ export function ProjectOverview({
|
||||
// 2. Projects exist but we haven't fetched health data yet (healthLoading with no data)
|
||||
// Don't show skeleton during background health polling when health data already exists
|
||||
const needsInitialSkeleton = loading || (healthLoading && projects.length > 0 && Object.keys(healthMap).length === 0);
|
||||
/*
|
||||
FNXC:DashboardHeader 2026-06-22-16:42:
|
||||
The Dashboard overview (projects, stats, filters, and charts/overview content) owns the shared top header. The Board view must stay headerless because its columns already consume the full board surface.
|
||||
*/
|
||||
const dashboardHeader = (
|
||||
<ViewHeader
|
||||
icon={LayoutGrid}
|
||||
title={t("dashboard.title", "Dashboard")}
|
||||
actions={(
|
||||
<button
|
||||
className="btn btn-primary btn-sm project-overview__add-btn"
|
||||
onClick={onAddProject}
|
||||
>
|
||||
<Plus size={14} />
|
||||
{t("projects.addProject", "Add Project")}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
// Show skeleton while loading
|
||||
if (needsInitialSkeleton) {
|
||||
return <ProjectGridSkeleton />;
|
||||
return (
|
||||
<div className="project-overview">
|
||||
{dashboardHeader}
|
||||
<div className="project-overview__body">
|
||||
<ProjectGridSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty state when no projects
|
||||
if (projects.length === 0) {
|
||||
return (
|
||||
<div className="project-overview project-overview--empty">
|
||||
<div className="project-empty-state">
|
||||
<div className="project-empty-state__icon">
|
||||
<Inbox size={48} />
|
||||
<div className="project-overview">
|
||||
{dashboardHeader}
|
||||
<div className="project-overview__body project-overview__body--empty">
|
||||
<div className="project-empty-state">
|
||||
<div className="project-empty-state__icon">
|
||||
<Inbox size={48} />
|
||||
</div>
|
||||
<h2 className="project-empty-state__title">{t("projects.noProjectsFound", "No Projects Found")}</h2>
|
||||
<p className="project-empty-state__description">
|
||||
{t("projects.emptyStateDescription", "Get started by adding your first project. Projects allow you to organize and track tasks across multiple repositories.")}
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary project-empty-state__cta"
|
||||
onClick={onAddProject}
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t("projects.addFirstProject", "Add Your First Project")}
|
||||
</button>
|
||||
</div>
|
||||
<h2 className="project-empty-state__title">{t("projects.noProjectsFound", "No Projects Found")}</h2>
|
||||
<p className="project-empty-state__description">
|
||||
{t("projects.emptyStateDescription", "Get started by adding your first project. Projects allow you to organize and track tasks across multiple repositories.")}
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary project-empty-state__cta"
|
||||
onClick={onAddProject}
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t("projects.addFirstProject", "Add Your First Project")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -267,12 +297,10 @@ export function ProjectOverview({
|
||||
|
||||
return (
|
||||
<div className="project-overview">
|
||||
{dashboardHeader}
|
||||
<div className="project-overview__body">
|
||||
{/* Header with stats */}
|
||||
<div className="project-overview__header">
|
||||
<h2 className="project-overview__title">
|
||||
<LayoutGrid size={20} />
|
||||
{t("projects.title", "Projects")}
|
||||
</h2>
|
||||
<div className="project-overview__stats">
|
||||
<div className="project-stat">
|
||||
<div className="project-stat__icon">
|
||||
@@ -324,13 +352,6 @@ export function ProjectOverview({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary project-overview__add-btn"
|
||||
onClick={onAddProject}
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t("projects.addProject", "Add Project")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter tabs */}
|
||||
@@ -450,6 +471,7 @@ export function ProjectOverview({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3226,6 +3226,16 @@ describe("ChatView CSS — active state edge highlights", () => {
|
||||
expect(activeScopeRule).not.toContain("inset");
|
||||
});
|
||||
|
||||
it("renders the header Direct/Rooms toggle with visible borders", async () => {
|
||||
const headerScopeRule = findRule(".chat-view-header-scope-toggle");
|
||||
const headerScopeButtonRule = findRule(".chat-view-header-scope-toggle .chat-sidebar-scope-btn");
|
||||
const headerActiveScopeRule = findRule(".chat-view-header-scope-toggle .chat-sidebar-scope-btn--active");
|
||||
|
||||
expect(headerScopeRule).toContain("border: 1px solid var(--border)");
|
||||
expect(headerScopeButtonRule).toContain("border: 1px solid transparent");
|
||||
expect(headerActiveScopeRule).toContain("border-color: var(--todo)");
|
||||
});
|
||||
|
||||
it("keeps active chat-row background without the removed left edge or offset", async () => {
|
||||
const activeSessionRule = findRule(".chat-session-item--active");
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("ProjectOverview", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Projects")).toBeDefined();
|
||||
expect(screen.getByRole("heading", { name: /Dashboard/ })).toBeDefined();
|
||||
});
|
||||
|
||||
it("displays project cards when projects provided", () => {
|
||||
|
||||
Reference in New Issue
Block a user