refactor(FN-851): remove expand control from TaskCard component

- Remove expand/collapse toggle and related state from TaskCard component
- Delete expand-related CSS styles from styles.css
- Remove expand control tests from TaskCard test suite
- Clean up unused expand-related props and handlers
This commit is contained in:
gsxdsm
2026-04-04 05:37:51 -07:00
parent 226ad37cfa
commit 0f02334ed9
5 changed files with 2 additions and 166 deletions

View File

@@ -42,7 +42,7 @@ AI-guided interactive planning for creating well-specified tasks from high-level
- `POST /api/planning/create-task` - Create task from summary (`{ sessionId }`)
### Task Management
- **Kanban Board**: Drag-and-drop task management across columns (Triage, Todo, In Progress, In Review, Done). Each card includes an explicit expand button (↗ icon in the card header) for opening task details — visible on hover for desktop, always visible on mobile. On touch devices, horizontal swipes that start on a card still scroll the board between columns; only quick taps open the task detail modal.
- **Kanban Board**: Drag-and-drop task management across columns (Triage, Todo, In Progress, In Review, Done). On touch devices, horizontal swipes that start on a card still scroll the board between columns; only quick taps open the task detail modal.
- **Inline Editing**: Quick-edit a task's description directly on the board for Triage and Todo columns. The editor opens as a taller multi-line editing area (4 visible lines) for comfortable editing of longer descriptions, and auto-grows to fit existing content. Double-click a card or use the pencil icon — visible on hover for desktop, always visible on mobile for touch accessibility. Inline editing changes only the description; the title is preserved. To edit both title and description, use the task detail modal.
- **Task Detail Editing**: Edit task title and description directly in the task detail modal. Click the pencil icon in the modal header (available for Triage and Todo tasks) to enter edit mode. Save and Cancel actions appear in the modal footer alongside a keyboard shortcut hint, keeping editing controls consistent with other modal action patterns.
- **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.

View File

@@ -8,7 +8,6 @@ vi.mock("lucide-react", () => ({
Link: () => null,
Clock: () => null,
Pencil: () => null,
Maximize2: () => null,
Layers: () => null,
ChevronDown: () => null,
Folder: () => null,

View File

@@ -1,5 +1,5 @@
import { memo, useCallback, useState, useRef, useEffect, useMemo } from "react";
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Maximize2 } from "lucide-react";
import { Link, Clock, Layers, Pencil, ChevronDown, Folder } from "lucide-react";
import type { Task, TaskDetail, Column, PrInfo, IssueInfo } from "@fusion/core";
import { fetchTaskDetail, uploadAttachment } from "../api";
import { GitHubBadge } from "./GitHubBadge";
@@ -463,11 +463,6 @@ function TaskCardComponent({
enterEditMode(e);
}, [enterEditMode]);
const handleExpandClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
void handleClick();
}, [handleClick]);
// Auto-resize textarea (similar to InlineCreateCard)
const handleDescChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditDescription(e.target.value);
@@ -583,14 +578,6 @@ function TaskCardComponent({
/>
)}
<div className="card-header-actions">
<button
className="card-expand-btn"
onClick={handleExpandClick}
title="Open task details"
aria-label="Open task details"
>
<Maximize2 size={12} />
</button>
{canEdit && (
<button
className="card-edit-btn"

View File

@@ -2515,13 +2515,6 @@ describe("TaskCard detail opening", () => {
});
});
it("renders an expand button for opening task details", () => {
const task = makeTask();
render(<TaskCard task={task} onOpenDetail={vi.fn()} addToast={noopToast} />);
expect(screen.getByRole("button", { name: /Open task details/i })).toBeDefined();
});
it("opens modal only once per card click", async () => {
const { fetchTaskDetail } = await import("../../api");
const mockFetch = vi.mocked(fetchTaskDetail);
@@ -2648,101 +2641,6 @@ describe("TaskCard detail opening", () => {
// Modal should NOT have opened
expect(onOpenDetail).not.toHaveBeenCalled();
});
it("renders an expand button in every column", () => {
const columns: Column[] = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
for (const column of columns) {
const task = makeTask({ column });
const { unmount } = render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
expect(screen.getByRole("button", { name: /Open task details/i })).toBeDefined();
unmount();
}
});
it("expand button opens task detail modal", async () => {
const { fetchTaskDetail } = await import("../../api");
const mockFetch = vi.mocked(fetchTaskDetail);
const mockDetail: TaskDetail = {
...makeTask({ id: "FN-099" }),
prompt: "",
attachments: [],
};
mockFetch.mockResolvedValueOnce(mockDetail);
const onOpenDetail = vi.fn();
render(
<TaskCard
task={makeTask()}
onOpenDetail={onOpenDetail}
addToast={noopToast}
/>
);
const expandBtn = screen.getByRole("button", { name: /Open task details/i });
fireEvent.click(expandBtn);
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith("FN-099", undefined);
expect(onOpenDetail).toHaveBeenCalledWith(mockDetail);
});
});
it("expand button click does not trigger card body click", async () => {
const { fetchTaskDetail } = await import("../../api");
const mockFetch = vi.mocked(fetchTaskDetail);
const mockDetail: TaskDetail = {
...makeTask({ id: "FN-099" }),
prompt: "",
attachments: [],
};
mockFetch.mockResolvedValueOnce(mockDetail);
const onOpenDetail = vi.fn();
const { container } = render(
<TaskCard
task={makeTask()}
onOpenDetail={onOpenDetail}
addToast={noopToast}
/>
);
const expandBtn = screen.getByRole("button", { name: /Open task details/i });
// Use a real click event to test stopPropagation
const clickEvent = new MouseEvent("click", { bubbles: true });
const stopSpy = vi.spyOn(clickEvent, "stopPropagation");
fireEvent(expandBtn, clickEvent);
expect(stopSpy).toHaveBeenCalled();
// The handler should still call fetchTaskDetail via handleClick
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith("FN-099", undefined);
});
});
it("expand button has correct styling class", () => {
render(
<TaskCard
task={makeTask()}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const expandBtn = screen.getByRole("button", { name: /Open task details/i });
expect(expandBtn.classList.contains("card-expand-btn")).toBe(true);
});
});
/**

View File

@@ -4205,38 +4205,6 @@ body {
outline-offset: 1px;
}
/* Expand button - visible on hover */
.card-expand-btn {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 0;
background: transparent;
border: none;
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s, color 0.15s;
}
.card:hover .card-expand-btn {
opacity: 1;
}
.card-expand-btn:hover {
background: var(--border);
color: var(--text);
}
.card-expand-btn:focus {
opacity: 1;
outline: 1px solid var(--todo);
outline-offset: 1px;
}
/* Archive/Unarchive buttons */
.card-archive-btn,
.card-unarchive-btn {
@@ -5193,22 +5161,6 @@ body {
width: 16px;
height: 16px;
}
/* Card expand button: always visible on mobile (no hover) */
.card-expand-btn {
opacity: 1;
width: 44px;
height: 44px;
margin-right: -8px;
margin-top: -8px;
margin-bottom: -8px;
border-radius: var(--radius-md);
}
.card-expand-btn svg {
width: 16px;
height: 16px;
}
}
/* === Tablet Responsive Tier (769px1024px) === */