feat(KB-095): add Hide Done toggle to List View

- Add Hide Done toggle that filters out both done and archived tasks
- Update ListView component with filter state and toggle UI
- Add comprehensive tests for toggle behavior including edge cases
- Document the Hide Done toggle behavior in README
This commit is contained in:
gsxdsm
2026-03-29 22:22:13 -07:00
parent f7784a577a
commit 1b5511c968
3 changed files with 170 additions and 38 deletions

View File

@@ -40,7 +40,7 @@ AI-guided interactive planning for creating well-specified tasks from high-level
### Task Management
- **Kanban Board**: Drag-and-drop task management across columns (Triage, Todo, In Progress, In Review, Done)
- **Inline Editing**: Quick-edit task title and description directly on the board for Triage and Todo columns. Double-click a card or use the pencil icon that appears on hover.
- **List View**: Alternative tabular view for tasks with sorting and filtering
- **List View**: Alternative tabular view for tasks with sorting and filtering. The "Hide Done" toggle hides both Done and Archived tasks for an active-work-only view.
- **Task Details**: View full task specifications, agent logs, and attachments
- **GitHub Import**: Import issues directly from GitHub repositories
- **PR Management**: Create and track pull requests for in-review tasks

View File

@@ -200,9 +200,10 @@ export function ListView({
)
: [...tasks];
// Then filter out done tasks if hideDoneTasks is enabled
if (hideDoneTasks) {
filtered = filtered.filter((t) => t.column !== "done");
// Then filter out done and archived tasks if hideDoneTasks is enabled
// BUT only when no specific column is selected (strict hide semantics)
if (hideDoneTasks && !selectedColumn) {
filtered = filtered.filter((t) => t.column !== "done" && t.column !== "archived");
}
// Then apply column filter if selected
@@ -247,16 +248,16 @@ export function ListView({
return Object.values(groupedTasks).reduce((sum, group) => sum + group.length, 0);
}, [groupedTasks]);
// Calculate done task counts for stats display
const doneTaskCount = useMemo(() => {
return tasks.filter((t) => t.column === "done").length;
// Calculate done and archived task counts for stats display
const completedTaskCount = useMemo(() => {
return tasks.filter((t) => t.column === "done" || t.column === "archived").length;
}, [tasks]);
// Calculate hidden done tasks count
const hiddenDoneCount = useMemo(() => {
// Calculate hidden done+archived tasks count
const hiddenCompletedCount = useMemo(() => {
if (!hideDoneTasks) return 0;
return doneTaskCount;
}, [hideDoneTasks, doneTaskCount]);
return completedTaskCount;
}, [hideDoneTasks, completedTaskCount]);
const handleRowClick = useCallback(
async (task: Task) => {
try {
@@ -397,8 +398,8 @@ export function ListView({
{selectedColumn
? `${filteredCount} of ${tasks.length} tasks in ${COLUMN_LABELS[selectedColumn]}`
: `${filteredCount} of ${tasks.length} tasks`}
{hiddenDoneCount > 0 && !selectedColumn && (
<span className="list-stats-hidden"> ({hiddenDoneCount} done hidden)</span>
{hiddenCompletedCount > 0 && !selectedColumn && (
<span className="list-stats-hidden"> ({hiddenCompletedCount} hidden)</span>
)}
{selectedColumn && (
<button
@@ -423,8 +424,9 @@ export function ListView({
<div className="list-drop-zones">
{COLUMNS.map((column) => {
const totalCount = tasks.filter((t) => t.column === column).length;
const visibleCount = hideDoneTasks && column === "done" ? 0 : totalCount;
const showPartial = hideDoneTasks && column === "done" && totalCount > 0;
const isCompletedColumn = column === "done" || column === "archived";
const visibleCount = hideDoneTasks && isCompletedColumn ? 0 : totalCount;
const showPartial = hideDoneTasks && isCompletedColumn && totalCount > 0;
return (
<div
@@ -499,8 +501,8 @@ export function ListView({
// When column filter is active, only show the selected column
if (selectedColumn && column !== selectedColumn) return null;
// Skip done column section when hideDoneTasks is enabled (unless it's the selected column)
if (hideDoneTasks && column === "done" && !selectedColumn) return null;
// Skip done and archived column sections when hideDoneTasks is enabled (unless it's the selected column)
if (hideDoneTasks && (column === "done" || column === "archived") && !selectedColumn) return null;
const columnTasks = groupedTasks[column];
const isEmpty = columnTasks.length === 0;

View File

@@ -1044,27 +1044,75 @@ describe("ListView Hide Done Tasks", () => {
expect(screen.getByText("KB-002")).toBeDefined();
});
it("shows done tasks when toggle is deactivated", () => {
it("hides archived tasks when toggle is activated", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done" }),
createMockTask({ id: "KB-001", column: "archived" }),
createMockTask({ id: "KB-002", column: "triage" }),
];
renderListView({ tasks });
// Click hide done button to hide done tasks
// Both tasks should be visible initially
expect(screen.getByText("KB-001")).toBeDefined();
expect(screen.getByText("KB-002")).toBeDefined();
// Click hide done button
const hideDoneButton = screen.getByRole("button", { name: /hide done/i });
fireEvent.click(hideDoneButton);
// Done task should be hidden
// Archived task should be hidden, triage task should still be visible
expect(screen.queryByText("KB-001")).toBeNull();
expect(screen.getByText("KB-002")).toBeDefined();
});
// Click again to show done tasks
fireEvent.click(hideDoneButton);
it("hides both done and archived tasks when toggle is activated", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done" }),
createMockTask({ id: "KB-002", column: "archived" }),
createMockTask({ id: "KB-003", column: "triage" }),
];
// Both tasks should be visible again
renderListView({ tasks });
// All tasks should be visible initially
expect(screen.getByText("KB-001")).toBeDefined();
expect(screen.getByText("KB-002")).toBeDefined();
expect(screen.getByText("KB-003")).toBeDefined();
// Click hide done button
const hideDoneButton = screen.getByRole("button", { name: /hide done/i });
fireEvent.click(hideDoneButton);
// Done and archived tasks should be hidden, triage task should remain visible
expect(screen.queryByText("KB-001")).toBeNull();
expect(screen.queryByText("KB-002")).toBeNull();
expect(screen.getByText("KB-003")).toBeDefined();
});
it("shows done and archived tasks when toggle is deactivated", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done" }),
createMockTask({ id: "KB-002", column: "archived" }),
createMockTask({ id: "KB-003", column: "triage" }),
];
renderListView({ tasks });
// Click hide done button to hide completed tasks
const hideDoneButton = screen.getByRole("button", { name: /hide done/i });
fireEvent.click(hideDoneButton);
// Completed tasks should be hidden
expect(screen.queryByText("KB-001")).toBeNull();
expect(screen.queryByText("KB-002")).toBeNull();
// Click again to show all tasks
fireEvent.click(hideDoneButton);
// All tasks should be visible again
expect(screen.getByText("KB-001")).toBeDefined();
expect(screen.getByText("KB-002")).toBeDefined();
expect(screen.getByText("KB-003")).toBeDefined();
});
it("persists hide done preference to localStorage", () => {
@@ -1085,23 +1133,25 @@ describe("ListView Hide Done Tasks", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done" }),
createMockTask({ id: "KB-002", column: "triage" }),
createMockTask({ id: "KB-002", column: "archived" }),
createMockTask({ id: "KB-003", column: "triage" }),
];
renderListView({ tasks });
// Button should show "Show Done" text since done tasks are hidden
expect(screen.getByRole("button", { name: /show done/i })).toBeDefined();
// Done task should be hidden initially
// Completed tasks should be hidden initially
expect(screen.queryByText("KB-001")).toBeNull();
expect(screen.getByText("KB-002")).toBeDefined();
expect(screen.queryByText("KB-002")).toBeNull();
expect(screen.getByText("KB-003")).toBeDefined();
});
it("updates stats text when done tasks are hidden", () => {
it("updates stats text when done and archived tasks are hidden", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done" }),
createMockTask({ id: "KB-002", column: "triage" }),
createMockTask({ id: "KB-003", column: "done" }),
createMockTask({ id: "KB-002", column: "archived" }),
createMockTask({ id: "KB-003", column: "triage" }),
];
renderListView({ tasks });
@@ -1115,13 +1165,14 @@ describe("ListView Hide Done Tasks", () => {
// Stats should show filtered count with hidden indicator
expect(screen.getByText("1 of 3 tasks")).toBeDefined();
expect(screen.getByText(/2 done hidden/)).toBeDefined();
expect(screen.getByText(/2 hidden/)).toBeDefined();
});
it("hides done column section header when hide done is active", () => {
it("hides done and archived column section headers when hide done is active", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done" }),
createMockTask({ id: "KB-002", column: "triage" }),
createMockTask({ id: "KB-002", column: "archived" }),
createMockTask({ id: "KB-003", column: "triage" }),
];
renderListView({ tasks });
@@ -1134,11 +1185,22 @@ describe("ListView Hide Done Tasks", () => {
const hideDoneButton = screen.getByRole("button", { name: /hide done/i });
fireEvent.click(hideDoneButton);
// Done section should be hidden - find section headers and verify done is not present
// Done and Archived sections should be hidden
const doneSection = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Done")
);
expect(doneSection).toBeUndefined();
const archivedSection = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Archived")
);
expect(archivedSection).toBeUndefined();
// Triage section should still be visible
const triageSection = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage")
);
expect(triageSection).toBeDefined();
});
it("shows done drop zone with count when hide done is active", () => {
@@ -1159,10 +1221,29 @@ describe("ListView Hide Done Tasks", () => {
expect(doneZone?.textContent).toContain("0 of 2");
});
it("shows archived drop zone with count when hide done is active", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "archived" }),
createMockTask({ id: "KB-002", column: "archived" }),
];
renderListView({ tasks });
// Click hide done button
const hideDoneButton = screen.getByRole("button", { name: /hide done/i });
fireEvent.click(hideDoneButton);
// Archived drop zone should still be visible with "X of Y" format
const archivedZone = document.querySelector('[data-column="archived"].list-drop-zone');
expect(archivedZone).toBeDefined();
expect(archivedZone?.textContent).toContain("0 of 2");
});
it("preserves hide done state through filter changes", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done", title: "Alpha" }),
createMockTask({ id: "KB-002", column: "triage", title: "Beta" }),
createMockTask({ id: "KB-002", column: "archived", title: "Beta" }),
createMockTask({ id: "KB-003", column: "triage", title: "Gamma" }),
];
renderListView({ tasks });
@@ -1173,12 +1254,61 @@ describe("ListView Hide Done Tasks", () => {
// Apply filter
const filterInput = screen.getByPlaceholderText("Filter by ID or title...");
fireEvent.change(filterInput, { target: { value: "Beta" } });
fireEvent.change(filterInput, { target: { value: "Gamma" } });
// Done task should remain hidden
// Completed tasks should remain hidden
expect(screen.queryByText("KB-001")).toBeNull();
expect(screen.queryByText("KB-002")).toBeNull();
// Filtered task should be visible
expect(screen.getByText("KB-002")).toBeDefined();
expect(screen.getByText("KB-003")).toBeDefined();
});
it("shows done section when selectedColumn is done even with hide done active", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "done", title: "Done Task" }),
createMockTask({ id: "KB-002", column: "triage", title: "Triage Task" }),
];
renderListView({ tasks });
// Enable hide done
const hideDoneButton = screen.getByRole("button", { name: /hide done/i });
fireEvent.click(hideDoneButton);
// Done task should be hidden
expect(screen.queryByText("KB-001")).toBeNull();
// Click on the done drop zone to select that column
const doneZone = document.querySelector('[data-column="done"].list-drop-zone')!;
fireEvent.click(doneZone);
// Done task should now be visible because selectedColumn overrides hide
expect(screen.getByText("KB-001")).toBeDefined();
expect(screen.queryByText("KB-002")).toBeNull();
});
it("shows archived section when selectedColumn is archived even with hide done active", () => {
const tasks = [
createMockTask({ id: "KB-001", column: "archived", title: "Archived Task" }),
createMockTask({ id: "KB-002", column: "triage", title: "Triage Task" }),
];
renderListView({ tasks });
// Enable hide done
const hideDoneButton = screen.getByRole("button", { name: /hide done/i });
fireEvent.click(hideDoneButton);
// Archived task should be hidden
expect(screen.queryByText("KB-001")).toBeNull();
// Click on the archived drop zone to select that column
const archivedZone = document.querySelector('[data-column="archived"].list-drop-zone')!;
fireEvent.click(archivedZone);
// Archived task should now be visible because selectedColumn overrides hide
expect(screen.getByText("KB-001")).toBeDefined();
expect(screen.queryByText("KB-002")).toBeNull();
});
});