feat(FN-973): move plan/subtask/refine buttons to disclosure panel in QuickEntryBox
- Redesign QuickEntryBox to use a collapsible disclosure panel for plan, subtask, and AI refine actions - Add disclosure toggle button (lightbulb icon) that expands/collapses the action panel - Move dependency picker and model overrides into the disclosure panel - Update ListView and QuickEntryBox tests to cover new disclosure panel behavior - Add CSS styles for disclosure panel transitions and layout
This commit is contained in:
@@ -732,111 +732,111 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
{/* AI-assisted refinement actions — always visible when expanded */}
|
||||
{isExpanded && !isSubmitting && (
|
||||
<div className="quick-entry-description-actions" data-testid="quick-entry-description-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={handlePlanClick}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
disabled={!description.trim()}
|
||||
data-testid="plan-button"
|
||||
title="Open planning mode with current description"
|
||||
>
|
||||
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Plan
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={handleSubtaskClick}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
disabled={!description.trim()}
|
||||
data-testid="subtask-button"
|
||||
title="Break down into AI-generated subtasks"
|
||||
>
|
||||
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Subtask
|
||||
</button>
|
||||
<div className="refine-trigger-wrap" ref={refineMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
|
||||
onClick={() => {
|
||||
setIsRefineMenuOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
// Compute position synchronously so the portal renders on first paint
|
||||
updateRefineMenuPosition();
|
||||
} else {
|
||||
setRefineMenuPosition(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
disabled={!description.trim() || isRefining}
|
||||
data-testid="refine-button"
|
||||
title="Refine description with AI"
|
||||
>
|
||||
<Sparkles size={12} style={{ verticalAlign: "middle" }} />
|
||||
{isRefining ? "Refining..." : "Refine"}
|
||||
</button>
|
||||
{isRefineMenuOpen && portalRoot && refineMenuPosition && createPortal(
|
||||
<div
|
||||
ref={refineMenuPortalRef}
|
||||
className="refine-menu refine-menu--portal"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: `${refineMenuPosition.top}px`,
|
||||
left: `${refineMenuPosition.left}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("clarify")}
|
||||
data-testid="refine-clarify"
|
||||
>
|
||||
<div className="refine-menu-item-title">Clarify</div>
|
||||
<div className="refine-menu-item-desc">Make the description clearer and more specific</div>
|
||||
</div>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("add-details")}
|
||||
data-testid="refine-add-details"
|
||||
>
|
||||
<div className="refine-menu-item-title">Add details</div>
|
||||
<div className="refine-menu-item-desc">Add implementation details and context</div>
|
||||
</div>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("expand")}
|
||||
data-testid="refine-expand"
|
||||
>
|
||||
<div className="refine-menu-item-title">Expand</div>
|
||||
<div className="refine-menu-item-desc">Expand into a more comprehensive description</div>
|
||||
</div>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("simplify")}
|
||||
data-testid="refine-simplify"
|
||||
>
|
||||
<div className="refine-menu-item-title">Simplify</div>
|
||||
<div className="refine-menu-item-desc">Simplify and make more concise</div>
|
||||
</div>
|
||||
</div>,
|
||||
portalRoot,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
id="quick-entry-controls"
|
||||
className="quick-entry-controls"
|
||||
hidden={!showExpandedControls}
|
||||
aria-hidden={!showExpandedControls}
|
||||
>
|
||||
{/* Plan, Subtask, Refine — consolidated action buttons */}
|
||||
{showExpandedControls && !isSubmitting && (
|
||||
<div className="quick-entry-actions" data-testid="quick-entry-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={handlePlanClick}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
disabled={!description.trim()}
|
||||
data-testid="plan-button"
|
||||
title="Open planning mode with current description"
|
||||
>
|
||||
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Plan
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={handleSubtaskClick}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
disabled={!description.trim()}
|
||||
data-testid="subtask-button"
|
||||
title="Break down into AI-generated subtasks"
|
||||
>
|
||||
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Subtask
|
||||
</button>
|
||||
<div className="refine-trigger-wrap" ref={refineMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
|
||||
onClick={() => {
|
||||
setIsRefineMenuOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
// Compute position synchronously so the portal renders on first paint
|
||||
updateRefineMenuPosition();
|
||||
} else {
|
||||
setRefineMenuPosition(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
disabled={!description.trim() || isRefining}
|
||||
data-testid="refine-button"
|
||||
title="Refine description with AI"
|
||||
>
|
||||
<Sparkles size={12} style={{ verticalAlign: "middle" }} />
|
||||
{isRefining ? "Refining..." : "Refine"}
|
||||
</button>
|
||||
{isRefineMenuOpen && portalRoot && refineMenuPosition && createPortal(
|
||||
<div
|
||||
ref={refineMenuPortalRef}
|
||||
className="refine-menu refine-menu--portal"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: `${refineMenuPosition.top}px`,
|
||||
left: `${refineMenuPosition.left}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("clarify")}
|
||||
data-testid="refine-clarify"
|
||||
>
|
||||
<div className="refine-menu-item-title">Clarify</div>
|
||||
<div className="refine-menu-item-desc">Make the description clearer and more specific</div>
|
||||
</div>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("add-details")}
|
||||
data-testid="refine-add-details"
|
||||
>
|
||||
<div className="refine-menu-item-title">Add details</div>
|
||||
<div className="refine-menu-item-desc">Add implementation details and context</div>
|
||||
</div>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("expand")}
|
||||
data-testid="refine-expand"
|
||||
>
|
||||
<div className="refine-menu-item-title">Expand</div>
|
||||
<div className="refine-menu-item-desc">Expand into a more comprehensive description</div>
|
||||
</div>
|
||||
<div
|
||||
className="refine-menu-item"
|
||||
onClick={() => handleRefine("simplify")}
|
||||
data-testid="refine-simplify"
|
||||
>
|
||||
<div className="refine-menu-item-title">Simplify</div>
|
||||
<div className="refine-menu-item-desc">Simplify and make more concise</div>
|
||||
</div>
|
||||
</div>,
|
||||
portalRoot,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="quick-entry-controls-left">
|
||||
{/* Deps button */}
|
||||
<div className="dep-trigger-wrap">
|
||||
|
||||
@@ -1463,11 +1463,7 @@ describe("ListView Quick Entry", () => {
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
// Open the actions dropdown to access Deps/Models/Save
|
||||
const actionsButton = screen.getByTestId("quick-entry-actions-button");
|
||||
fireEvent.click(actionsButton);
|
||||
|
||||
// Model selector button should be visible
|
||||
// Model selector button should be directly visible in the disclosure panel
|
||||
const modelButton = await screen.findByTestId("quick-entry-models-button");
|
||||
expect(modelButton).toBeDefined();
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
@@ -1484,11 +1480,7 @@ describe("ListView Quick Entry", () => {
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
// Open the actions dropdown to access Deps/Models/Save
|
||||
const actionsButton = screen.getByTestId("quick-entry-actions-button");
|
||||
fireEvent.click(actionsButton);
|
||||
|
||||
// Dependency selector button should be visible
|
||||
// Dependency selector button should be directly visible in the disclosure panel
|
||||
const depsButton = await screen.findByTestId("quick-entry-deps-button");
|
||||
expect(depsButton).toBeDefined();
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
@@ -2084,10 +2076,7 @@ describe("ListView - Bulk Selection", () => {
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
// Open the actions dropdown to access Deps/Models/Save
|
||||
const actionsButton = screen.getByTestId("quick-entry-actions-button");
|
||||
fireEvent.click(actionsButton);
|
||||
|
||||
// Model selector button is directly visible in the disclosure panel
|
||||
const modelButton = await screen.findByTestId("quick-entry-models-button");
|
||||
fireEvent.click(modelButton);
|
||||
|
||||
|
||||
@@ -1650,11 +1650,11 @@ describe("QuickEntryBox", () => {
|
||||
|
||||
// Now controls should be visible
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
// Deps/Models/Save should be directly accessible (no actions dropdown needed)
|
||||
// Deps/Models/Save should be directly accessible
|
||||
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
// Plan/Subtask are always visible in description-actions when expanded
|
||||
// Plan/Subtask are in the actions area inside the disclosure panel
|
||||
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("subtask-button")).toBeTruthy();
|
||||
});
|
||||
@@ -1688,57 +1688,57 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Description-adjacent actions layout (FN-781)", () => {
|
||||
it("renders Plan, Subtask, and Refine in description-actions area when expanded", () => {
|
||||
describe("Consolidated actions layout (FN-781, FN-973)", () => {
|
||||
it("renders Plan, Subtask, and Refine in actions area inside controls panel", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
// The description-actions container should exist
|
||||
expect(screen.getByTestId("quick-entry-description-actions")).toBeTruthy();
|
||||
// The actions container should exist inside the controls panel
|
||||
expect(screen.getByTestId("quick-entry-actions")).toBeTruthy();
|
||||
|
||||
// Plan, Subtask, and Refine buttons should be inside it
|
||||
const actionsContainer = screen.getByTestId("quick-entry-description-actions");
|
||||
const actionsContainer = screen.getByTestId("quick-entry-actions");
|
||||
expect(actionsContainer.contains(screen.getByTestId("plan-button"))).toBe(true);
|
||||
expect(actionsContainer.contains(screen.getByTestId("subtask-button"))).toBe(true);
|
||||
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not render description-actions when not expanded", () => {
|
||||
it("does not render actions when not expanded", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Description actions should not exist when collapsed
|
||||
expect(screen.queryByTestId("quick-entry-description-actions")).toBeNull();
|
||||
// Actions should not exist when collapsed
|
||||
expect(screen.queryByTestId("quick-entry-actions")).toBeNull();
|
||||
});
|
||||
|
||||
it("Save button is in the controls panel, not description-actions", () => {
|
||||
it("Save button is in the controls panel, not actions area", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
|
||||
const actionsContainer = screen.getByTestId("quick-entry-description-actions");
|
||||
const actionsContainer = screen.getByTestId("quick-entry-actions");
|
||||
const saveButton = screen.getByTestId("save-button");
|
||||
|
||||
// Save button should NOT be in the description-actions area (it's in the controls panel)
|
||||
// Save button should NOT be in the actions area (it's in the controls-left panel)
|
||||
expect(actionsContainer.contains(saveButton)).toBe(false);
|
||||
});
|
||||
|
||||
it("Deps and Models buttons are in the controls panel, not description-actions", () => {
|
||||
it("Deps and Models buttons are in the controls panel, not actions area", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task text" } });
|
||||
|
||||
const actionsContainer = screen.getByTestId("quick-entry-description-actions");
|
||||
const actionsContainer = screen.getByTestId("quick-entry-actions");
|
||||
const depsButton = screen.getByTestId("quick-entry-deps-button");
|
||||
const modelsButton = screen.getByTestId("quick-entry-models-button");
|
||||
|
||||
// Deps and Models should NOT be in the description-actions area (they're in the controls panel)
|
||||
// Deps and Models should NOT be in the actions area (they're in the controls-left panel)
|
||||
expect(actionsContainer.contains(depsButton)).toBe(false);
|
||||
expect(actionsContainer.contains(modelsButton)).toBe(false);
|
||||
});
|
||||
|
||||
it("Plan button disabled state still works when in description-actions", () => {
|
||||
it("Plan button disabled state still works in actions area", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
@@ -1750,6 +1750,35 @@ describe("QuickEntryBox", () => {
|
||||
fireEvent.change(textarea, { target: { value: "Some task" } });
|
||||
expect((screen.getByTestId("plan-button") as HTMLButtonElement).disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("shows Plan/Subtask/Refine together with Deps/Models/Save when disclosure is expanded and text entered", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "A task with all features" } });
|
||||
|
||||
// All 6 buttons should be visible inside the controls panel
|
||||
const controlsPanel = document.getElementById("quick-entry-controls");
|
||||
expect(controlsPanel?.hasAttribute("hidden")).toBe(false);
|
||||
|
||||
// Plan/Subtask/Refine are in actions area
|
||||
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("subtask-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("refine-button")).toBeTruthy();
|
||||
|
||||
// Deps/Models/Save are in controls-left area
|
||||
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
|
||||
// All should be inside the same controls panel
|
||||
expect(controlsPanel?.contains(screen.getByTestId("plan-button"))).toBe(true);
|
||||
expect(controlsPanel?.contains(screen.getByTestId("subtask-button"))).toBe(true);
|
||||
expect(controlsPanel?.contains(screen.getByTestId("refine-button"))).toBe(true);
|
||||
expect(controlsPanel?.contains(screen.getByTestId("quick-entry-deps-button"))).toBe(true);
|
||||
expect(controlsPanel?.contains(screen.getByTestId("quick-entry-models-button"))).toBe(true);
|
||||
expect(controlsPanel?.contains(screen.getByTestId("save-button"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Preset selection through model menu", () => {
|
||||
|
||||
@@ -12415,12 +12415,11 @@ html .column.drag-over * {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* AI-assisted description actions — sits between textarea and expanded controls */
|
||||
.quick-entry-description-actions {
|
||||
/* Consolidated action buttons (Plan, Subtask, Refine) inside disclosure panel */
|
||||
.quick-entry-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user