feat(KB-127): add Quick Entry box to ListView for rapid task creation
- Add QuickEntryBox component to ListView for inline task creation - Wire up Quick Create functionality in App.tsx - Add comprehensive tests for Quick Entry feature in ListView - Remove legacy TaskDetailModal edit button and related styles - Clean up unused store methods and update vitest configs
This commit is contained in:
@@ -242,6 +242,7 @@ function AppInner() {
|
|||||||
isCreating={isListInlineCreating}
|
isCreating={isListInlineCreating}
|
||||||
onCancelCreate={handleListInlineCreateCancel}
|
onCancelCreate={handleListInlineCreateCancel}
|
||||||
onCreateTask={handleListInlineCreate}
|
onCreateTask={handleListInlineCreate}
|
||||||
|
onQuickCreate={handleQuickCreate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{detailTask && (
|
{detailTask && (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Task, TaskDetail, Column, TaskStep, TaskCreateInput } from "@kb/co
|
|||||||
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 { InlineCreateCard } from "./InlineCreateCard";
|
||||||
|
import { QuickEntryBox } from "./QuickEntryBox";
|
||||||
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> = {
|
||||||
@@ -34,6 +35,7 @@ interface ListViewProps {
|
|||||||
onCancelCreate?: () => void;
|
onCancelCreate?: () => void;
|
||||||
onCreateTask?: (input: TaskCreateInput) => Promise<Task>;
|
onCreateTask?: (input: TaskCreateInput) => Promise<Task>;
|
||||||
onNewTask?: () => void;
|
onNewTask?: () => void;
|
||||||
|
onQuickCreate?: (description: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStepProgress(steps: TaskStep[]): string {
|
function getStepProgress(steps: TaskStep[]): string {
|
||||||
@@ -58,6 +60,7 @@ export function ListView({
|
|||||||
isCreating,
|
isCreating,
|
||||||
onCancelCreate,
|
onCancelCreate,
|
||||||
onCreateTask,
|
onCreateTask,
|
||||||
|
onQuickCreate,
|
||||||
}: ListViewProps) {
|
}: ListViewProps) {
|
||||||
const [sortField, setSortField] = useState<SortField>("id");
|
const [sortField, setSortField] = useState<SortField>("id");
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
|
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
|
||||||
@@ -350,6 +353,11 @@ export function ListView({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{onQuickCreate && (
|
||||||
|
<div className="list-quick-entry">
|
||||||
|
<QuickEntryBox onCreate={onQuickCreate} addToast={addToast} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="list-column-toggle" ref={columnDropdownRef}>
|
<div className="list-column-toggle" ref={columnDropdownRef}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm"
|
className="btn btn-sm"
|
||||||
|
|||||||
@@ -1401,3 +1401,98 @@ describe("ListView Inline Create Card", () => {
|
|||||||
expect(saveButton?.textContent).toBe("Save");
|
expect(saveButton?.textContent).toBe("Save");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ListView Quick Entry", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders QuickEntryBox when onQuickCreate is provided", () => {
|
||||||
|
const mockOnQuickCreate = vi.fn().mockResolvedValue(undefined);
|
||||||
|
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||||
|
|
||||||
|
// Quick entry box should be visible
|
||||||
|
const quickEntry = screen.getByTestId("quick-entry-box");
|
||||||
|
expect(quickEntry).toBeDefined();
|
||||||
|
|
||||||
|
// Input should be visible
|
||||||
|
const input = screen.getByTestId("quick-entry-input");
|
||||||
|
expect(input).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render QuickEntryBox when onQuickCreate is not provided", () => {
|
||||||
|
renderListView({ onQuickCreate: undefined });
|
||||||
|
|
||||||
|
// Quick entry box should not be visible
|
||||||
|
const quickEntry = screen.queryByTestId("quick-entry-box");
|
||||||
|
expect(quickEntry).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onQuickCreate with description when Enter is pressed", async () => {
|
||||||
|
const mockOnQuickCreate = vi.fn().mockResolvedValue(undefined);
|
||||||
|
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||||
|
|
||||||
|
const input = screen.getByTestId("quick-entry-input");
|
||||||
|
fireEvent.change(input, { target: { value: "New quick task" } });
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockOnQuickCreate).toHaveBeenCalledWith("New quick task");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears input after successful quick create", async () => {
|
||||||
|
const mockOnQuickCreate = vi.fn().mockResolvedValue(undefined);
|
||||||
|
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||||
|
|
||||||
|
const input = screen.getByTestId("quick-entry-input") as HTMLInputElement;
|
||||||
|
fireEvent.change(input, { target: { value: "Task to create" } });
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockOnQuickCreate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(input.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows error toast when onQuickCreate fails and keeps input content", async () => {
|
||||||
|
const mockOnQuickCreate = vi.fn().mockRejectedValue(new Error("Create failed"));
|
||||||
|
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||||
|
|
||||||
|
const input = screen.getByTestId("quick-entry-input") as HTMLInputElement;
|
||||||
|
fireEvent.change(input, { target: { value: "Failed task" } });
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockAddToast).toHaveBeenCalledWith("Create failed", "error");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Input content should be preserved for retry
|
||||||
|
expect(input.value).toBe("Failed task");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims whitespace when creating task via quick entry", async () => {
|
||||||
|
const mockOnQuickCreate = vi.fn().mockResolvedValue(undefined);
|
||||||
|
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||||
|
|
||||||
|
const input = screen.getByTestId("quick-entry-input");
|
||||||
|
fireEvent.change(input, { target: { value: " Task with spaces " } });
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockOnQuickCreate).toHaveBeenCalledWith("Task with spaces");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not submit on Enter if input is empty", async () => {
|
||||||
|
const mockOnQuickCreate = vi.fn().mockResolvedValue(undefined);
|
||||||
|
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||||
|
|
||||||
|
const input = screen.getByTestId("quick-entry-input");
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
expect(mockOnQuickCreate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -3142,6 +3142,15 @@ body {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-quick-entry {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-quick-entry .quick-entry-box {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.list-column-dropdown {
|
.list-column-dropdown {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user