feat(FN-1555): remove quick-entry textarea expand/fullscreen affordance
- Remove expand/compress chevron button from QuickEntryBox - Remove isExpanded state and resize logic - Remove fullscreen toggle functionality - Remove related test cases for expand/collapse behavior
This commit is contained in:
@@ -4,7 +4,7 @@ import type { ToastType } from "../hooks/useToast";
|
||||
import type { Task, TaskCreateInput, Settings } from "@fusion/core";
|
||||
import type { ModelInfo, RefinementType, Agent } from "../api";
|
||||
import { fetchModels, fetchSettings, refineText, getRefineErrorMessage, updateGlobalSettings, fetchAgents, uploadAttachment } from "../api";
|
||||
import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot } from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
|
||||
@@ -92,10 +92,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// isDisclosureExpanded controls visibility of the controls panel (Deps, Models, etc.)
|
||||
// Always starts collapsed — user must explicitly toggle each session
|
||||
const [isDisclosureExpanded, setIsDisclosureExpanded] = useState(false);
|
||||
// isDescriptionExpanded controls fullscreen description editing mode
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
// Track textarea focus for expand button visibility
|
||||
const [isDescriptionFocused, setIsDescriptionFocused] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const justResetRef = useRef(false);
|
||||
@@ -264,15 +260,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// In fullscreen mode, CSS handles sizing via flex: 1, don't set inline height
|
||||
if (isDescriptionExpanded) return;
|
||||
|
||||
// Reset height to auto to get accurate scrollHeight
|
||||
textarea.style.height = "auto";
|
||||
// Set to scrollHeight (capped at max-height via CSS)
|
||||
const newHeight = Math.min(textarea.scrollHeight, 200);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
}, [isDescriptionExpanded]);
|
||||
}, []);
|
||||
|
||||
// Resize when description changes (not in fullscreen mode since CSS handles it)
|
||||
useEffect(() => {
|
||||
@@ -375,7 +368,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
setIsRefining(false);
|
||||
setIsExpanded(false); // Collapse textarea height on reset
|
||||
setIsDisclosureExpanded(false); // Always reset controls to collapsed after creation
|
||||
setIsDescriptionExpanded(false); // Exit fullscreen mode on reset
|
||||
justResetRef.current = true;
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
@@ -483,7 +475,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.shiftKey && (isExpanded || isDescriptionExpanded)) {
|
||||
if (e.shiftKey && isExpanded) {
|
||||
// Allow Shift+Enter to insert newline when expanded or in fullscreen mode
|
||||
// Don't prevent default - let the newline be inserted
|
||||
return;
|
||||
@@ -493,11 +485,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
handleSubmit();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
// Exit fullscreen mode first - highest priority
|
||||
if (isDescriptionExpanded) {
|
||||
setIsDescriptionExpanded(false);
|
||||
return;
|
||||
}
|
||||
// Close model submenu first if open
|
||||
if (activeModelSubmenu) {
|
||||
setActiveModelSubmenu(null);
|
||||
@@ -543,7 +530,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
handleSubmit,
|
||||
description,
|
||||
isExpanded,
|
||||
isDescriptionExpanded,
|
||||
showDeps,
|
||||
showAgentPicker,
|
||||
isModelMenuOpen,
|
||||
@@ -560,8 +546,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
if (justResetRef.current) {
|
||||
justResetRef.current = false;
|
||||
}
|
||||
// Track description focus for expand button visibility
|
||||
setIsDescriptionFocused(false);
|
||||
}, []);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
@@ -569,8 +553,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
if (autoExpand) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
// Track description focus for expand button visibility
|
||||
setIsDescriptionFocused(true);
|
||||
}, [autoExpand]);
|
||||
|
||||
const toggleDep = useCallback((id: string) => {
|
||||
@@ -922,108 +904,40 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleDescriptionExpand = useCallback(() => {
|
||||
setIsDescriptionExpanded((prev) => {
|
||||
const next = !prev;
|
||||
// Focus the fullscreen textarea after it renders
|
||||
if (next && textareaRef.current) {
|
||||
// Use setTimeout to ensure focus happens after the textarea is rendered
|
||||
setTimeout(() => textareaRef.current?.focus(), 0);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDescriptionFullscreenKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!isDescriptionExpanded || e.key !== "Escape") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDescriptionExpanded(false);
|
||||
}, [isDescriptionExpanded]);
|
||||
|
||||
return (
|
||||
<div className={`quick-entry-box ${isDisclosureExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}`} data-testid="quick-entry-box">
|
||||
<div
|
||||
className={`description-with-refine${isDescriptionExpanded ? " description--fullscreen" : ""}`}
|
||||
onKeyDown={handleDescriptionFullscreenKeyDown}
|
||||
>
|
||||
{isDescriptionExpanded && (
|
||||
<div className="description-fullscreen-header">
|
||||
<span>Editing Description</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm description-expand-btn"
|
||||
onClick={handleToggleDescriptionExpand}
|
||||
aria-label="Collapse description"
|
||||
title="Collapse description"
|
||||
data-testid="quick-entry-collapse"
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!isDescriptionExpanded && (
|
||||
<div className="quick-entry-main-row">
|
||||
<div className="quick-entry-textarea-wrap">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={`quick-entry-input ${isExpanded ? "quick-entry-input--expanded" : ""}`}
|
||||
placeholder={isSubmitting ? "Creating..." : "Add a task..."}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
data-testid="quick-entry-input"
|
||||
rows={1}
|
||||
aria-controls="quick-entry-controls"
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
/>
|
||||
{isDescriptionFocused && description.trim() && !isDisabled && !isSubmitting && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm quick-entry-expand-btn"
|
||||
onClick={handleToggleDescriptionExpand}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
aria-label="Expand description"
|
||||
title="Expand description"
|
||||
data-testid="quick-entry-expand"
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm quick-entry-toggle"
|
||||
onClick={toggleExpanded}
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
<div className="description-with-refine">
|
||||
<div className="quick-entry-main-row">
|
||||
<div className="quick-entry-textarea-wrap">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={`quick-entry-input ${isExpanded ? "quick-entry-input--expanded" : ""}`}
|
||||
placeholder={isSubmitting ? "Creating..." : "Add a task..."}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
data-testid="quick-entry-input"
|
||||
rows={1}
|
||||
aria-controls="quick-entry-controls"
|
||||
data-testid="quick-entry-toggle"
|
||||
title={isDisclosureExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</button>
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isDescriptionExpanded && (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="quick-entry-input quick-entry-input--fullscreen"
|
||||
placeholder={isSubmitting ? "Creating..." : "Add a task..."}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
data-testid="quick-entry-input-fullscreen"
|
||||
rows={10}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm quick-entry-toggle"
|
||||
onClick={toggleExpanded}
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
aria-controls="quick-entry-controls"
|
||||
data-testid="quick-entry-toggle"
|
||||
title={isDisclosureExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="quick-entry-controls"
|
||||
|
||||
@@ -2435,8 +2435,8 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("description fullscreen expansion", () => {
|
||||
it("shows expand button when textarea is focused and has content", async () => {
|
||||
describe("description expand functionality removed", () => {
|
||||
it("does not render expand button when textarea is focused and has content", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
@@ -2444,125 +2444,74 @@ describe("QuickEntryBox", () => {
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test task description" } });
|
||||
|
||||
// Expand button should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-expand")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("hides expand button when textarea is empty", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus without typing
|
||||
fireEvent.focus(textarea);
|
||||
|
||||
// Expand button should not be visible when empty
|
||||
// Expand button should NOT be present
|
||||
expect(screen.queryByTestId("quick-entry-expand")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides expand button when textarea is blurred", async () => {
|
||||
it("does not render collapse button", () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus, type, then blur
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test content" } });
|
||||
fireEvent.blur(textarea);
|
||||
|
||||
// Expand button should be hidden after blur
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(screen.queryByTestId("quick-entry-expand")).not.toBeInTheDocument();
|
||||
// Collapse button should NOT be present
|
||||
expect(screen.queryByTestId("quick-entry-collapse")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enters fullscreen mode when expand button is clicked", async () => {
|
||||
it("does not render fullscreen textarea", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Fullscreen textarea should NOT be present
|
||||
expect(screen.queryByTestId("quick-entry-input-fullscreen")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("task creation from primary textarea still works", async () => {
|
||||
const onCreate = vi.fn().mockResolvedValue({ id: "FN-001", title: "", description: "Test task", column: "triage" });
|
||||
renderQuickEntryBox({ onCreate });
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus and type content
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
fireEvent.change(textarea, { target: { value: "Test task description" } });
|
||||
|
||||
// Click expand button
|
||||
// Submit with Enter key
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
// onCreate should have been called
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
});
|
||||
|
||||
// Fullscreen textarea should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Collapse button should be visible
|
||||
expect(screen.getByTestId("quick-entry-collapse")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("exits fullscreen mode when collapse button is clicked", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Enter fullscreen mode
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click collapse button
|
||||
fireEvent.click(screen.getByTestId("quick-entry-collapse"));
|
||||
|
||||
// Fullscreen textarea should be hidden
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("quick-entry-input-fullscreen")).not.toBeInTheDocument();
|
||||
expect(onCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ description: "Test task description" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("exits fullscreen mode when Escape key is pressed in fullscreen", async () => {
|
||||
renderQuickEntryBox({});
|
||||
it("textarea expands on focus with autoExpand", async () => {
|
||||
renderQuickEntryBox({ autoExpand: true });
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Enter fullscreen mode
|
||||
// Focus should trigger expansion
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Press Escape
|
||||
const fullscreenTextarea = screen.getByTestId("quick-entry-input-fullscreen");
|
||||
fireEvent.keyDown(fullscreenTextarea, { key: "Escape" });
|
||||
|
||||
// Fullscreen should be exited
|
||||
// Textarea should have expanded class
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("quick-entry-input-fullscreen")).not.toBeInTheDocument();
|
||||
expect(textarea).toHaveClass("quick-entry-input--expanded");
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves description text when entering fullscreen", async () => {
|
||||
it("Shift+Enter inserts newline in expanded textarea", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Type some content
|
||||
// Expand textarea first
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "My test task description" } });
|
||||
|
||||
// Enter fullscreen
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
expect(textarea).toHaveClass("quick-entry-input--expanded");
|
||||
});
|
||||
|
||||
// Check fullscreen textarea has the content
|
||||
// Type and press Shift+Enter
|
||||
fireEvent.change(textarea, { target: { value: "Line 1" } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter", shiftKey: true });
|
||||
|
||||
// Content should still be present (newline was inserted)
|
||||
await waitFor(() => {
|
||||
const fullscreenTextarea = screen.getByTestId("quick-entry-input-fullscreen") as HTMLTextAreaElement;
|
||||
expect(fullscreenTextarea.value).toBe("My test task description");
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("Line 1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user