feat(KB-257): add localStorage persistence for QuickEntryBox draft input
- Persist draft task input in localStorage to prevent data loss on refresh - Restore saved draft when component mounts - Clear localStorage entry when task is successfully created - Add comprehensive tests for persistence behavior - Remove fixed positioning styles from ListView component
This commit is contained in:
@@ -6,6 +6,8 @@ import { fetchModels } from "../api";
|
||||
import { Link, Brain, Lightbulb, ListTree } from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
|
||||
const STORAGE_KEY = "kb-quick-entry-text";
|
||||
|
||||
interface QuickEntryBoxProps {
|
||||
onCreate?: (input: TaskCreateInput) => Promise<void>;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
@@ -42,7 +44,12 @@ function parseModelSelection(value: string): { provider?: string; modelId?: stri
|
||||
}
|
||||
|
||||
export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown }: QuickEntryBoxProps) {
|
||||
const [description, setDescription] = useState("");
|
||||
const [description, setDescription] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem(STORAGE_KEY) || "";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -115,6 +122,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
[loadedModels],
|
||||
);
|
||||
|
||||
// Persist description to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORAGE_KEY, description);
|
||||
}
|
||||
}, [description]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -173,6 +187,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
}
|
||||
// Clear localStorage when form is reset (after successful creation)
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
@@ -233,13 +251,17 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
setShowModels(false);
|
||||
return;
|
||||
}
|
||||
// Clear non-empty input on Escape
|
||||
// Clear non-empty input on Escape and clear localStorage
|
||||
if (description.trim()) {
|
||||
setDescription("");
|
||||
// Reset height
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
}
|
||||
// Clear localStorage when user explicitly clears input
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
// Collapse on escape
|
||||
resetForm();
|
||||
|
||||
@@ -619,4 +619,98 @@ describe("QuickEntryBox", () => {
|
||||
expect(screen.queryByTestId("subtask-button")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("localStorage persistence", () => {
|
||||
beforeEach(() => {
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("restores description from localStorage on mount", () => {
|
||||
// Pre-populate localStorage
|
||||
localStorage.setItem("kb-quick-entry-text", "Saved task description");
|
||||
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Should restore the saved description
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("Saved task description");
|
||||
});
|
||||
|
||||
it("updates localStorage when typing", async () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Typing this task" } });
|
||||
|
||||
// Wait for the useEffect to run
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Typing this task");
|
||||
});
|
||||
});
|
||||
|
||||
it("clears localStorage after successful task creation", async () => {
|
||||
const { props } = renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Type something to set localStorage
|
||||
fireEvent.change(textarea, { target: { value: "Task to create" } });
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Task to create");
|
||||
});
|
||||
|
||||
// Submit the task
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// localStorage should be cleared
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBeNull();
|
||||
});
|
||||
|
||||
it("clears localStorage when Escape clears non-empty input", async () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Type something to set localStorage
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Task to clear" } });
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Task to clear");
|
||||
});
|
||||
|
||||
// Press Escape to clear the input
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
|
||||
// Input and localStorage should be cleared
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("");
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not clear localStorage on first Escape when closing dropdowns", () => {
|
||||
renderQuickEntryBox();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Type something and open dropdown
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Task with dropdown" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-deps-button"));
|
||||
|
||||
// localStorage should have the value
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Task with dropdown");
|
||||
|
||||
// First Escape closes dropdown but keeps input
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
|
||||
// Input and localStorage should be preserved
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("Task with dropdown");
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Task with dropdown");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user