feat(KB-046): add InlineCreateCard to ListView triage section
- Add InlineCreateCard component to ListView for inline task creation - Integrate isCreating, onCancelCreate, and onCreateTask props to ListView - Render InlineCreateCard in triage section when isCreating is true - Add comprehensive tests for InlineCreateCard integration in ListView - Handle blur, Escape key, and Enter key for task creation flow
This commit is contained in:
@@ -3,6 +3,7 @@ import { LayoutGrid, List as ListIcon, ArrowUpDown, ArrowUp, ArrowDown, Search,
|
|||||||
import type { Task, TaskDetail, Column, TaskStep } from "@kb/core";
|
import type { Task, TaskDetail, Column, TaskStep } from "@kb/core";
|
||||||
import { COLUMN_LABELS, COLUMNS } from "@kb/core";
|
import { COLUMN_LABELS, COLUMNS } from "@kb/core";
|
||||||
import { fetchTaskDetail } from "../api";
|
import { fetchTaskDetail } from "../api";
|
||||||
|
import { InlineCreateCard } from "./InlineCreateCard";
|
||||||
import type { ToastType } from "../hooks/useToast";
|
import type { ToastType } from "../hooks/useToast";
|
||||||
|
|
||||||
const COLUMN_COLOR_MAP: Record<Column, string> = {
|
const COLUMN_COLOR_MAP: Record<Column, string> = {
|
||||||
@@ -58,6 +59,9 @@ export function ListView({
|
|||||||
addToast,
|
addToast,
|
||||||
globalPaused,
|
globalPaused,
|
||||||
onNewTask,
|
onNewTask,
|
||||||
|
isCreating,
|
||||||
|
onCancelCreate,
|
||||||
|
onCreateTask,
|
||||||
}: ListViewProps) {
|
}: ListViewProps) {
|
||||||
const [sortField, setSortField] = useState<SortField>("createdAt");
|
const [sortField, setSortField] = useState<SortField>("createdAt");
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
|
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
|
||||||
@@ -419,7 +423,7 @@ export function ListView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list-table-container">
|
<div className="list-table-container">
|
||||||
{filteredCount === 0 ? (
|
{filteredCount === 0 && !isCreating ? (
|
||||||
<div className="list-empty">
|
<div className="list-empty">
|
||||||
{filter ? "No tasks match your filter" : "No tasks yet"}
|
{filter ? "No tasks match your filter" : "No tasks yet"}
|
||||||
</div>
|
</div>
|
||||||
@@ -473,8 +477,8 @@ export function ListView({
|
|||||||
const columnTasks = groupedTasks[column];
|
const columnTasks = groupedTasks[column];
|
||||||
const isEmpty = columnTasks.length === 0;
|
const isEmpty = columnTasks.length === 0;
|
||||||
|
|
||||||
// When filtering, hide empty sections entirely
|
// When filtering, hide empty sections entirely (except triage when creating)
|
||||||
if (filter && isEmpty) return null;
|
if (filter && isEmpty && !(column === "triage" && isCreating)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={column}>
|
<Fragment key={column}>
|
||||||
@@ -487,6 +491,20 @@ export function ListView({
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{/* Inline Create Card for Triage column */}
|
||||||
|
{column === "triage" && isCreating && onCancelCreate && onCreateTask && (
|
||||||
|
<tr className="list-inline-create-row">
|
||||||
|
<td colSpan={visibleColumns.size} className="list-inline-create-cell">
|
||||||
|
<InlineCreateCard
|
||||||
|
tasks={tasks}
|
||||||
|
onSubmit={onCreateTask}
|
||||||
|
onCancel={onCancelCreate}
|
||||||
|
addToast={addToast}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Task Rows */}
|
{/* Task Rows */}
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<tr className="list-section-empty">
|
<tr className="list-section-empty">
|
||||||
|
|||||||
@@ -854,6 +854,7 @@ describe("ListView Column Visibility", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("ListView Hide Done Tasks", () => {
|
describe("ListView Hide Done Tasks", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -1025,3 +1026,87 @@ describe("ListView Hide Done Tasks", () => {
|
|||||||
expect(screen.getByText("KB-002")).toBeDefined();
|
expect(screen.getByText("KB-002")).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ListView Inline Create Card", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows InlineCreateCard when isCreating is true", () => {
|
||||||
|
renderListView({ isCreating: true, onCancelCreate: vi.fn(), onCreateTask: vi.fn() });
|
||||||
|
|
||||||
|
// The inline creation card should be visible with its textarea
|
||||||
|
expect(screen.getByPlaceholderText("What needs to be done?")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show InlineCreateCard when isCreating is false", () => {
|
||||||
|
renderListView({ isCreating: false, onCancelCreate: vi.fn(), onCreateTask: vi.fn() });
|
||||||
|
|
||||||
|
// The inline creation card should not be visible
|
||||||
|
expect(screen.queryByPlaceholderText("What needs to be done?")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show InlineCreateCard when onCancelCreate is not provided", () => {
|
||||||
|
renderListView({ isCreating: true, onCreateTask: vi.fn() });
|
||||||
|
|
||||||
|
// The inline creation card should not be visible without onCancelCreate
|
||||||
|
expect(screen.queryByPlaceholderText("What needs to be done?")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show InlineCreateCard when onCreateTask is not provided", () => {
|
||||||
|
renderListView({ isCreating: true, onCancelCreate: vi.fn() });
|
||||||
|
|
||||||
|
// The inline creation card should not be visible without onCreateTask
|
||||||
|
expect(screen.queryByPlaceholderText("What needs to be done?")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onCreateTask with triage column when task is submitted from inline card", async () => {
|
||||||
|
const mockOnCreateTask = vi.fn().mockResolvedValue(createMockTask({ id: "KB-002" }));
|
||||||
|
renderListView({ isCreating: true, onCancelCreate: vi.fn(), onCreateTask: mockOnCreateTask });
|
||||||
|
|
||||||
|
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||||
|
fireEvent.change(textarea, { target: { value: "New task description" } });
|
||||||
|
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockOnCreateTask).toHaveBeenCalledWith({
|
||||||
|
description: "New task description",
|
||||||
|
column: "triage",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onCancelCreate when inline card is cancelled via blur", () => {
|
||||||
|
const mockOnCancelCreate = vi.fn();
|
||||||
|
renderListView({ isCreating: true, onCancelCreate: mockOnCancelCreate, onCreateTask: vi.fn() });
|
||||||
|
|
||||||
|
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||||
|
textarea.focus();
|
||||||
|
fireEvent.focusOut(textarea, { relatedTarget: null });
|
||||||
|
|
||||||
|
expect(mockOnCancelCreate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onCancelCreate when inline card is cancelled via Escape key", () => {
|
||||||
|
const mockOnCancelCreate = vi.fn();
|
||||||
|
renderListView({ isCreating: true, onCancelCreate: mockOnCancelCreate, onCreateTask: vi.fn() });
|
||||||
|
|
||||||
|
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||||
|
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||||
|
|
||||||
|
expect(mockOnCancelCreate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders InlineCreateCard in triage section with correct colSpan", () => {
|
||||||
|
renderListView({ isCreating: true, onCancelCreate: vi.fn(), onCreateTask: vi.fn() });
|
||||||
|
|
||||||
|
// Find the inline create row
|
||||||
|
const inlineCreateRow = document.querySelector(".list-inline-create-row");
|
||||||
|
expect(inlineCreateRow).toBeTruthy();
|
||||||
|
|
||||||
|
// Check that the cell has the correct colSpan (8 columns by default)
|
||||||
|
const inlineCreateCell = document.querySelector(".list-inline-create-cell");
|
||||||
|
expect(inlineCreateCell).toBeTruthy();
|
||||||
|
expect(inlineCreateCell?.getAttribute("colspan")).toBe("8");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user