feat(KB-280): add Save button to QuickEntryBox
- Add Save button to QuickEntryBox component for manual task creation - Include loading state and disabled state during submission - Add comprehensive tests for Save button functionality (114 lines) - Add changeset for the new save button feature
This commit is contained in:
@@ -3,7 +3,7 @@ import type { ToastType } from "../hooks/useToast";
|
||||
import type { Task, TaskCreateInput } from "@kb/core";
|
||||
import type { ModelInfo, RefinementType } from "../api";
|
||||
import { fetchModels, refineText, getRefineErrorMessage } from "../api";
|
||||
import { Link, Brain, Lightbulb, ListTree, Sparkles } from "lucide-react";
|
||||
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save } from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
|
||||
const STORAGE_KEY = "kb-quick-entry-text";
|
||||
@@ -394,6 +394,17 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
resetForm();
|
||||
}, [description, onSubtaskBreakdown, addToast, resetForm]);
|
||||
|
||||
const handleSaveClick = useCallback(() => {
|
||||
const trimmed = description.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
// Explicitly save to localStorage (even though auto-save already does this)
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORAGE_KEY, trimmed);
|
||||
}
|
||||
addToast("Draft saved", "success");
|
||||
}, [description, addToast]);
|
||||
|
||||
const handleRefine = useCallback(async (type: RefinementType) => {
|
||||
const trimmed = description.trim();
|
||||
if (!trimmed || isRefining) return;
|
||||
@@ -619,6 +630,17 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
<ListTree size={12} style={{ verticalAlign: "middle" }} />
|
||||
Subtask
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={handleSaveClick}
|
||||
disabled={!description.trim() || isSubmitting}
|
||||
data-testid="save-button"
|
||||
title="Save draft to browser storage"
|
||||
>
|
||||
<Save size={12} style={{ verticalAlign: "middle" }} />
|
||||
Save
|
||||
</button>
|
||||
<div className="refine-trigger-wrap" ref={refineMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -76,6 +76,7 @@ vi.mock("lucide-react", () => ({
|
||||
Lightbulb: () => null,
|
||||
ListTree: () => null,
|
||||
Sparkles: () => null,
|
||||
Save: () => null,
|
||||
}));
|
||||
|
||||
function renderQuickEntryBox(props = {}) {
|
||||
@@ -923,4 +924,117 @@ describe("QuickEntryBox", () => {
|
||||
expect(screen.queryByTestId("refine-clarify")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Save button", () => {
|
||||
it("shows save button when text is entered", () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Initially, save button is not visible
|
||||
expect(screen.queryByTestId("save-button")).toBeNull();
|
||||
|
||||
// Focus and type something
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
|
||||
// Now the save button should be visible
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("save button is disabled when textarea is empty", () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus and type something
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Some text" } });
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
|
||||
// Clear the input
|
||||
fireEvent.change(textarea, { target: { value: "" } });
|
||||
|
||||
// Button should be hidden or disabled when input is empty
|
||||
const saveButton = screen.queryByTestId("save-button") as HTMLButtonElement | null;
|
||||
if (saveButton) {
|
||||
expect(saveButton.disabled).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("save button is disabled during submission", async () => {
|
||||
const { props } = renderQuickEntryBox();
|
||||
// Slow down the promise to see loading state
|
||||
props.onCreate.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
|
||||
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "New task" } });
|
||||
|
||||
// Start submission with Enter key
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
// During submission, button should be disabled
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.queryByTestId("save-button") as HTMLButtonElement | null;
|
||||
if (saveButton) {
|
||||
expect(saveButton.disabled).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("clicking save button persists to localStorage", async () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Draft task description" } });
|
||||
|
||||
// Click the save button
|
||||
fireEvent.click(screen.getByTestId("save-button"));
|
||||
|
||||
// localStorage should have the value
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Draft task description");
|
||||
});
|
||||
});
|
||||
|
||||
it("clicking save button shows success toast", async () => {
|
||||
const { props } = renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
|
||||
// Click the save button
|
||||
fireEvent.click(screen.getByTestId("save-button"));
|
||||
|
||||
// Success toast should be shown
|
||||
await waitFor(() => {
|
||||
expect(props.addToast).toHaveBeenCalledWith("Draft saved", "success");
|
||||
});
|
||||
});
|
||||
|
||||
it("save button has correct test id", () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
|
||||
// Button should have data-testid="save-button"
|
||||
const saveButton = screen.getByTestId("save-button");
|
||||
expect(saveButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("save button has correct title attribute", () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
|
||||
const saveButton = screen.getByTestId("save-button");
|
||||
expect(saveButton.getAttribute("title")).toBe("Save draft to browser storage");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user