feat(FN-3223): add compact desktop sidebar controls for list view

The merge delivers three major feature clusters: hardened research lifecycle with idempotent cancel/retry and SSE support across the orchestrator and routes layer; unified header action clusters in AgentDetailView integrating planning mode disclosures; and a compact desktop sidebar for list view con

Fusion-Task-Id: FN-3223
This commit is contained in:
Fusion
2026-05-03 09:36:31 -07:00
committed by gsxdsm
parent 7bb1380e1a
commit f50e7c5305
4 changed files with 324 additions and 216 deletions

View File

@@ -16,10 +16,62 @@
background: var(--surface);
}
.list-stats {
font-size: 12px;
.list-sidebar-controls {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-md) var(--space-xl);
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.list-sidebar-controls__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
}
.list-sidebar-summary-chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
}
.list-sidebar-chip {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--border) 60%, transparent);
color: var(--text-muted);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
}
.list-view-options-panel {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-sm);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--card);
}
.list-toolbar-mobile-options {
padding: 0 var(--space-md) var(--space-sm);
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.list-view-options-columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-xs);
}
.list-stats {
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
color: var(--text-muted);
margin-left: auto;
}
/* Section expand/collapse controls */
@@ -240,20 +292,21 @@
.list-drop-zones {
display: flex;
gap: var(--space-sm);
padding: var(--space-md) var(--space-xl);
border-bottom: 1px solid var(--border);
background: var(--surface);
overflow-x: auto;
}
.list-drop-zones--sidebar {
flex-wrap: wrap;
}
.list-drop-zone {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-md);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
flex-shrink: 0;
@@ -970,6 +1023,16 @@
display: none;
}
.list-toolbar-mobile-options .list-drop-zones {
display: flex;
flex-wrap: wrap;
padding: 0;
}
.list-toolbar-mobile-options .drop-zone-label {
display: inline;
}
/* Toolbar stacking on mobile */
.list-toolbar {
padding: var(--space-sm) var(--space-md);

View File

@@ -286,9 +286,7 @@ export function ListView({
}
}, [collapsedSections, projectId]);
// Column dropdown state
const [columnDropdownOpen, setColumnDropdownOpen] = useState(false);
const columnDropdownRef = useRef<HTMLDivElement>(null);
const [viewOptionsOpen, setViewOptionsOpen] = useState(false);
// Selection state - initialize from localStorage
const [bulkEditEnabled, setBulkEditEnabled] = useState(false);
@@ -438,30 +436,6 @@ export function ListView({
});
}, []);
// Close dropdown when clicking outside
useEffect(() => {
if (!columnDropdownOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (columnDropdownRef.current && !columnDropdownRef.current.contains(e.target as Node)) {
setColumnDropdownOpen(false);
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setColumnDropdownOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
};
}, [columnDropdownOpen]);
// Column display labels
const COLUMN_LABELS_MAP: Record<ListColumn, string> = {
@@ -959,148 +933,39 @@ export function ListView({
);
};
return (
<div className="list-view">
<div className="list-toolbar">
<div className="list-column-toggle" ref={columnDropdownRef}>
<button
className="btn btn-sm"
onClick={() => setColumnDropdownOpen((prev) => !prev)}
aria-expanded={columnDropdownOpen}
aria-haspopup="menu"
>
<Columns3 size={14} />
Columns
</button>
{columnDropdownOpen && (
<div className="list-column-dropdown" role="menu">
{ALL_LIST_COLUMNS.map((column) => {
const isVisible = visibleColumns.has(column);
const isLastVisible = isVisible && visibleColumns.size === 1;
return (
<label
key={column}
className={`list-column-dropdown-item${isLastVisible ? " disabled" : ""}`}
role="menuitem"
title={isLastVisible ? "At least one column must be visible" : ""}
>
<input
type="checkbox"
checked={isVisible}
onChange={() => toggleColumn(column)}
disabled={isLastVisible}
/>
<span>{COLUMN_LABELS_MAP[column]}</span>
</label>
);
})}
</div>
)}
</div>
<button
className="btn btn-sm list-hide-done-toggle"
onClick={() => setHideDoneTasks((prev) => !prev)}
aria-pressed={hideDoneTasks}
title={hideDoneTasks ? "Show done tasks" : "Hide done tasks"}
>
{hideDoneTasks ? <Eye size={14} /> : <EyeOff size={14} />}
{hideDoneTasks ? "Show Done" : "Hide Done"}
</button>
<div className="list-stats">
{selectedColumn
? `${filteredCount} of ${tasks.length} tasks in ${COLUMN_LABELS[selectedColumn]}`
: `${filteredCount} of ${tasks.length} tasks`}
{hiddenCompletedCount > 0 && !selectedColumn && (
<span className="list-stats-hidden"> ({hiddenCompletedCount} hidden)</span>
)}
{selectedColumn && (
<button
className="btn btn-sm list-clear-column-filter-btn"
onClick={clearColumnFilter}
aria-label="Clear column filter"
const renderViewOptionsPanel = (panelId: string) => (
<div id={panelId} className="list-view-options-panel">
<div className="list-view-options-columns">
{ALL_LIST_COLUMNS.map((column) => {
const isVisible = visibleColumns.has(column);
const isLastVisible = isVisible && visibleColumns.size === 1;
return (
<label
key={column}
className={`list-column-dropdown-item${isLastVisible ? " disabled" : ""}`}
title={isLastVisible ? "At least one column must be visible" : ""}
>
Clear
</button>
)}
</div>
<button className="btn btn-sm" onClick={toggleBulkEdit} aria-pressed={bulkEditEnabled}>
{bulkEditEnabled ? "Done Editing" : "Bulk Edit"}
</button>
{bulkEditEnabled && selectedTaskIds.size > 0 && (
<div className="list-selection-stats">
<span className="selection-count">{selectedTaskIds.size} selected</span>
<button className="btn btn-sm btn-link" onClick={clearSelection}>
Clear
</button>
</div>
)}
{/* Bulk Edit Toolbar */}
{bulkEditEnabled && selectedTaskIds.size > 0 && availableModels && availableModels.length > 0 && (
<div className="bulk-edit-toolbar">
<span className="bulk-edit-label">Bulk Edit Models &amp; Node:</span>
<div className="bulk-edit-dropdown">
<CustomModelDropdown
models={availableModels}
value={executorModel}
onChange={setExecutorModel}
label="Executor Model"
noChangeValue="__no_change__"
noChangeLabel="No change"
favoriteProviders={favoriteProviders}
onToggleFavorite={onToggleFavorite}
favoriteModels={favoriteModels}
onToggleModelFavorite={onToggleModelFavorite}
<input
type="checkbox"
checked={isVisible}
onChange={() => toggleColumn(column)}
disabled={isLastVisible}
/>
</div>
<div className="bulk-edit-dropdown">
<CustomModelDropdown
models={availableModels}
value={validatorModel}
onChange={setValidatorModel}
label="Reviewer Model"
noChangeValue="__no_change__"
noChangeLabel="No change"
favoriteProviders={favoriteProviders}
onToggleFavorite={onToggleFavorite}
favoriteModels={favoriteModels}
onToggleModelFavorite={onToggleModelFavorite}
/>
</div>
<div className="bulk-edit-dropdown bulk-edit-node-wrap">
<select
className="select bulk-node-select"
value={nodeOverride}
onChange={(e) => setNodeOverride(e.target.value)}
aria-label="Node Override"
disabled={isLoadingNodes}
>
<option value="__no_change__">No change</option>
<option value="">Use project default</option>
{availableNodes.map((node) => (
<option key={node.id} value={node.id}>
{`${getNodeStatusSymbol(node.status)} ${node.name || node.id} (${getNodeStatusLabel(node.status)})`}
</option>
))}
</select>
{selectedOverrideNode ? <NodeHealthDot status={selectedOverrideNode.status} showLabel /> : null}
</div>
<button
className="btn btn-primary btn-sm bulk-edit-apply-btn"
onClick={handleApplyBulkUpdate}
disabled={isApplying || (executorModel === "__no_change__" && validatorModel === "__no_change__" && nodeOverride === "__no_change__")}
>
{isApplying ? "Applying..." : "Apply"}
</button>
</div>
)}
{onNewTask ? (
<button className="btn btn-task-create btn-sm" onClick={onNewTask}>
+ New Task
</button>
) : null}
<span>{COLUMN_LABELS_MAP[column]}</span>
</label>
);
})}
</div>
<div className="list-drop-zones">
<button
className="btn btn-sm list-hide-done-toggle"
onClick={() => setHideDoneTasks((prev) => !prev)}
aria-pressed={hideDoneTasks}
title={hideDoneTasks ? "Show done tasks" : "Hide done tasks"}
>
{hideDoneTasks ? <Eye size={14} /> : <EyeOff size={14} />}
{hideDoneTasks ? "Show Done" : "Hide Done"}
</button>
<div className="list-drop-zones list-drop-zones--sidebar">
{COLUMNS.map((column) => {
const totalCount = tasks.filter((t) => t.column === column).length;
const isCompletedColumn = column === "done" || column === "archived";
@@ -1117,7 +982,7 @@ export function ListView({
onDrop={(e) => handleColumnDrop(e, column)}
data-column={column}
>
<span className="drop-zone-dot" style={{ background: COLUMN_COLOR_MAP[column] }} />
<span className={`list-section-dot dot-${column}`} />
<span className="drop-zone-label">{COLUMN_LABELS[column]}</span>
<span className="drop-zone-count">
{showPartial ? `${visibleCount} of ${totalCount}` : totalCount}
@@ -1126,6 +991,42 @@ export function ListView({
);
})}
</div>
</div>
);
return (
<div className="list-view">
{isMobile && (
<>
<div className="list-toolbar">
<button className="btn btn-sm" onClick={toggleBulkEdit} aria-pressed={bulkEditEnabled}>
{bulkEditEnabled ? "Done Editing" : "Bulk Edit"}
</button>
{onNewTask ? (
<button className="btn btn-task-create btn-sm" onClick={onNewTask}>
+ New Task
</button>
) : null}
<button
className="btn btn-sm list-view-options-toggle"
onClick={() => setViewOptionsOpen((prev) => !prev)}
aria-expanded={viewOptionsOpen}
aria-controls="list-view-options-panel-mobile"
>
<Columns3 size={14} />
View options
</button>
<div className="list-stats">
{selectedColumn
? `${filteredCount} of ${tasks.length} tasks in ${COLUMN_LABELS[selectedColumn]}`
: `${filteredCount} of ${tasks.length} tasks`}
</div>
</div>
{viewOptionsOpen ? (
<div className="list-toolbar-mobile-options">{renderViewOptionsPanel("list-view-options-panel-mobile")}</div>
) : null}
</>
)}
<div className="list-table-container">
<div className={isMobile ? "" : "list-split-layout"} data-testid={isMobile ? undefined : "list-split-layout"} ref={splitLayoutRef}>
@@ -1135,6 +1036,114 @@ export function ListView({
ref={splitSidebarRef}
style={isMobile ? undefined : { width: `${sidebarWidth}px` }}
>
{!isMobile && (
<aside className="list-sidebar-controls" aria-label="List controls">
<div className="list-sidebar-controls__header">
<p className="list-stats">
{selectedColumn
? `${filteredCount} of ${tasks.length} tasks in ${COLUMN_LABELS[selectedColumn]}`
: `${filteredCount} of ${tasks.length} tasks`}
{hiddenCompletedCount > 0 && !selectedColumn && (
<span className="list-stats-hidden"> ({hiddenCompletedCount} hidden)</span>
)}
</p>
<div className="list-sidebar-controls__actions">
{onNewTask ? (
<button className="btn btn-task-create btn-sm" onClick={onNewTask}>
+ New Task
</button>
) : null}
<button className="btn btn-sm" onClick={toggleBulkEdit} aria-pressed={bulkEditEnabled}>
{bulkEditEnabled ? "Done Editing" : "Bulk Edit"}
</button>
</div>
<div className="list-sidebar-summary-chips">
{selectedColumn ? (
<button className="btn btn-sm" onClick={clearColumnFilter} aria-label="Clear column filter">
{`Filter: ${COLUMN_LABELS[selectedColumn]}`}
</button>
) : null}
{hideDoneTasks ? <span className="list-sidebar-chip">Done hidden</span> : null}
{bulkEditEnabled ? (
<span className="list-sidebar-chip">Bulk edit</span>
) : null}
{bulkEditEnabled && selectedTaskIds.size > 0 ? (
<button className="btn btn-sm" onClick={clearSelection}>
{`${selectedTaskIds.size} selected`}
</button>
) : null}
</div>
</div>
<button
className="btn btn-sm list-view-options-toggle"
onClick={() => setViewOptionsOpen((prev) => !prev)}
aria-expanded={viewOptionsOpen}
aria-controls="list-view-options-panel"
>
<Columns3 size={14} />
View options
</button>
{viewOptionsOpen && renderViewOptionsPanel("list-view-options-panel")}
{bulkEditEnabled && selectedTaskIds.size > 0 && availableModels && availableModels.length > 0 && (
<div className="bulk-edit-toolbar">
<span className="bulk-edit-label">Bulk Edit Models &amp; Node:</span>
<div className="bulk-edit-dropdown">
<CustomModelDropdown
models={availableModels}
value={executorModel}
onChange={setExecutorModel}
label="Executor Model"
noChangeValue="__no_change__"
noChangeLabel="No change"
favoriteProviders={favoriteProviders}
onToggleFavorite={onToggleFavorite}
favoriteModels={favoriteModels}
onToggleModelFavorite={onToggleModelFavorite}
/>
</div>
<div className="bulk-edit-dropdown">
<CustomModelDropdown
models={availableModels}
value={validatorModel}
onChange={setValidatorModel}
label="Reviewer Model"
noChangeValue="__no_change__"
noChangeLabel="No change"
favoriteProviders={favoriteProviders}
onToggleFavorite={onToggleFavorite}
favoriteModels={favoriteModels}
onToggleModelFavorite={onToggleModelFavorite}
/>
</div>
<div className="bulk-edit-dropdown bulk-edit-node-wrap">
<select
className="select bulk-node-select"
value={nodeOverride}
onChange={(e) => setNodeOverride(e.target.value)}
aria-label="Node Override"
disabled={isLoadingNodes}
>
<option value="__no_change__">No change</option>
<option value="">Use project default</option>
{availableNodes.map((node) => (
<option key={node.id} value={node.id}>
{`${getNodeStatusSymbol(node.status)} ${node.name || node.id} (${getNodeStatusLabel(node.status)})`}
</option>
))}
</select>
{selectedOverrideNode ? <NodeHealthDot status={selectedOverrideNode.status} showLabel /> : null}
</div>
<button
className="btn btn-primary btn-sm bulk-edit-apply-btn"
onClick={handleApplyBulkUpdate}
disabled={isApplying || (executorModel === "__no_change__" && validatorModel === "__no_change__" && nodeOverride === "__no_change__")}
>
{isApplying ? "Applying..." : "Apply"}
</button>
</div>
)}
</aside>
)}
<div className="list-quick-entry-above-table">
<QuickEntryBox
onCreate={onQuickCreate ?? (async () => addToast("Task creation not available", "error"))}

View File

@@ -50,7 +50,10 @@ const createMockTask = (overrides: Partial<Task> = {}): Task => ({
...overrides,
});
const renderListView = (props: Partial<React.ComponentProps<typeof ListView>> = {}) => {
const renderListView = (
props: Partial<React.ComponentProps<typeof ListView>> = {},
options: { openViewOptions?: boolean } = {},
) => {
const defaultProps = {
tasks: [],
onMoveTask: vi.fn(async () => createMockTask()),
@@ -66,7 +69,14 @@ const renderListView = (props: Partial<React.ComponentProps<typeof ListView>> =
projectId: TEST_PROJECT_ID,
};
return render(<ListView {...defaultProps} {...props} />);
const result = render(<ListView {...defaultProps} {...props} />);
if (options.openViewOptions ?? true) {
const viewOptionsToggle = screen.queryByRole("button", { name: /view options/i });
if (viewOptionsToggle) {
fireEvent.click(viewOptionsToggle);
}
}
return result;
};
const enterBulkEditMode = () => {
@@ -144,7 +154,28 @@ describe("ListView", () => {
it("renders without crashing", () => {
renderListView();
// The search/filter is now in the header, not in the list view toolbar
expect(screen.getByText("Columns")).toBeDefined();
expect(screen.getByText("View options")).toBeDefined();
});
it("keeps view options collapsed by default on desktop", () => {
renderListView({}, { openViewOptions: false });
const toggle = screen.getByRole("button", { name: /view options/i });
expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(document.getElementById("list-view-options-panel")).toBeNull();
});
it("shows compact summary chips while view options stay collapsed", () => {
localStorage.setItem(scopedStorageKey("kb-dashboard-hide-done"), "true");
const tasks = [
createMockTask({ id: "FN-001", column: "done" }),
createMockTask({ id: "FN-002", column: "triage" }),
];
renderListView({ tasks }, { openViewOptions: false });
expect(screen.getByText("Done hidden")).toBeDefined();
expect(document.getElementById("list-view-options-panel")).toBeNull();
});
it("displays tasks in table format", () => {
@@ -285,6 +316,25 @@ describe("ListView", () => {
viewportSpy.mockRestore();
});
it("exposes view options controls on mobile", () => {
const viewportSpy = mockMobileViewport();
localStorage.setItem(scopedStorageKey("kb-dashboard-hide-done"), "false");
renderListView({}, { openViewOptions: false });
const toggle = screen.getByRole("button", { name: /view options/i });
expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(document.getElementById("list-view-options-panel-mobile")).toBeNull();
fireEvent.click(toggle);
expect(screen.getByLabelText("Title")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /hide done/i }));
expect(localStorage.getItem(scopedStorageKey("kb-dashboard-hide-done"))).toBe("true");
viewportSpy.mockRestore();
});
it("keeps embedded selection visible when filters hide the selected row", async () => {
const viewportSpy = mockDesktopViewport();
const tasks = [
@@ -473,7 +523,7 @@ describe("ListView", () => {
renderListView({ tasks });
// First click - ascending
const titleHeader = screen.getByText("Title");
const titleHeader = screen.getByRole("columnheader", { name: /title/i });
fireEvent.click(titleHeader);
// Get all data rows (excluding section headers by using data-id attribute)
@@ -500,7 +550,7 @@ describe("ListView", () => {
renderListView({ tasks });
const columnHeader = screen.getByText("Column");
const columnHeader = screen.getByRole("columnheader", { name: /column/i });
fireEvent.click(columnHeader);
// Rows are rendered in fixed column-section order.
@@ -519,7 +569,7 @@ describe("ListView", () => {
renderListView({ tasks });
const statusHeader = screen.getByText("Status");
const statusHeader = screen.getByRole("columnheader", { name: /status/i });
fireEvent.click(statusHeader);
// Get data rows - sorted by status alphabetically
@@ -1088,7 +1138,7 @@ describe("ListView", () => {
renderListView({ tasks });
// Sort by title
const titleHeader = screen.getByText("Title");
const titleHeader = screen.getByRole("columnheader", { name: /title/i });
fireEvent.click(titleHeader);
// Get only data rows within the triage section
@@ -1281,36 +1331,31 @@ describe("ListView Column Visibility", () => {
localStorage.clear();
});
it("renders column toggle button", () => {
it("renders view options toggle button", () => {
renderListView();
const columnsButton = screen.getByRole("button", { name: /columns/i });
const columnsButton = screen.getByRole("button", { name: /view options/i });
expect(columnsButton).toBeDefined();
});
it("opens column dropdown when toggle clicked", () => {
renderListView();
renderListView({}, { openViewOptions: false });
const columnsButton = screen.getByRole("button", { name: /columns/i });
const columnsButton = screen.getByRole("button", { name: /view options/i });
fireEvent.click(columnsButton);
// Dropdown should be visible with checkboxes for each column
expect(columnsButton).toHaveAttribute("aria-expanded", "true");
expect(screen.queryByText("ID")).toBeNull();
expect(screen.getByText("Title")).toBeDefined();
expect(screen.getByText("Status")).toBeDefined();
expect(screen.getByText("Column")).toBeDefined();
expect(screen.getByText("Dependencies")).toBeDefined();
expect(screen.getByText("Progress")).toBeDefined();
expect(screen.getByLabelText("Title")).toBeDefined();
expect(screen.getByLabelText("Status")).toBeDefined();
expect(screen.getByLabelText("Column")).toBeDefined();
expect(screen.getByLabelText("Dependencies")).toBeDefined();
expect(screen.getByLabelText("Progress")).toBeDefined();
});
it("hides column when unchecked in dropdown", () => {
const tasks = [createMockTask({ id: "FN-001", title: "Test Task" })];
renderListView({ tasks });
// Open dropdown
const columnsButton = screen.getByRole("button", { name: /columns/i });
fireEvent.click(columnsButton);
// Uncheck the Title column
const checkboxes = screen.getAllByRole("checkbox");
const titleCheckbox = checkboxes.find(
@@ -1328,10 +1373,6 @@ describe("ListView Column Visibility", () => {
const tasks = [createMockTask({ id: "FN-001", title: "Test Task" })];
renderListView({ tasks });
// Open dropdown
const columnsButton = screen.getByRole("button", { name: /columns/i });
fireEvent.click(columnsButton);
// Find and uncheck the Title column
const checkboxes = screen.getAllByRole("checkbox");
const titleCheckbox = checkboxes.find(
@@ -1360,9 +1401,7 @@ describe("ListView Column Visibility", () => {
const tasks = [createMockTask({ id: "FN-001", title: "Test Task" })];
renderListView({ tasks });
// Open dropdown and uncheck Title
const columnsButton = screen.getByRole("button", { name: /columns/i });
fireEvent.click(columnsButton);
// Uncheck Title
const titleCheckbox = screen.getByLabelText("Title");
fireEvent.click(titleCheckbox);
@@ -1389,9 +1428,7 @@ describe("ListView Column Visibility", () => {
it("prevents hiding all columns (at least one stays visible)", () => {
renderListView();
// Open dropdown
const columnsButton = screen.getByRole("button", { name: /columns/i });
fireEvent.click(columnsButton);
// View options panel already open
// Get all checkboxes and try to uncheck all except one
const checkboxes = screen.getAllByRole("checkbox");
@@ -1419,8 +1456,6 @@ describe("ListView Column Visibility", () => {
renderListView({ tasks });
// Hide some columns
const columnsButton = screen.getByRole("button", { name: /columns/i });
fireEvent.click(columnsButton);
const checkboxes = screen.getAllByRole("checkbox");
const columnCheckbox = checkboxes.find(
cb => cb.parentElement?.textContent?.includes("Column")
@@ -1786,7 +1821,7 @@ describe("ListView Quick Entry", () => {
const tableContainer = document.querySelector(".list-table-container");
// QuickEntryBox should not be inside toolbar
expect(toolbar?.contains(quickEntry)).toBe(false);
expect(toolbar?.contains(quickEntry)).not.toBe(true);
// QuickEntryBox should be inside the new quick-entry area
expect(quickEntryArea?.contains(quickEntry)).toBe(true);
// QuickEntryBox should be inside the table container (parent of quick-entry area)
@@ -2095,7 +2130,7 @@ describe("ListView Collapsible Sections", () => {
fireEvent.click(triageHeader!);
// Sort by title
const titleHeader = screen.getByText("Title");
const titleHeader = screen.getByRole("columnheader", { name: /title/i });
fireEvent.click(titleHeader);
// Get sorted rows and verify sorting still works
@@ -2256,7 +2291,7 @@ describe("ListView - Bulk Selection", () => {
fireEvent.click(checkbox);
expect(screen.getByText("1 selected")).toBeDefined();
const clearButton = screen.getByText("Clear");
const clearButton = screen.getByRole("button", { name: /selected/i });
fireEvent.click(clearButton);
expect(screen.queryByText("1 selected")).toBeNull();
@@ -2273,7 +2308,7 @@ describe("ListView - Bulk Selection", () => {
const selectAllCheckbox = screen.getByLabelText("Select all visible tasks");
fireEvent.click(selectAllCheckbox);
expect(screen.getByText("2 selected")).toBeDefined();
expect(screen.getByRole("button", { name: /selected/i })).toBeDefined();
});
it("accepts favoriteProviders and favoriteModels props", () => {
@@ -2786,8 +2821,8 @@ describe("ListView - Bulk Selection", () => {
enterBulkEditMode();
fireEvent.click(screen.getByLabelText("Select FN-002"));
expect(screen.getByText("2 selected")).toBeInTheDocument();
expect(screen.getByText("Bulk Edit Models & Node:")).toBeInTheDocument();
expect((screen.getByLabelText("Select FN-001") as HTMLInputElement).checked).toBe(true);
expect((screen.getByLabelText("Select FN-002") as HTMLInputElement).checked).toBe(true);
});
it("applies agent-active class to mobile cards when task is in-progress and not paused/failed", () => {