refactor(FN-962): simplify QuickEntryBox by removing actions dropdown
- Remove the actions dropdown (⋯) and move Plan, Subtask, and other action buttons directly into the disclosure panel - Simplify QuickEntryBox component by eliminating dropdown menu state management and nested menu logic - Update QuickEntryBox tests to reflect the simplified UI with direct button access - Remove dependency/model selection submenus previously housed in the actions dropdown
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 { Task, TaskCreateInput, Settings } from "@fusion/core";
|
||||||
import type { ModelInfo, RefinementType } from "../api";
|
import type { ModelInfo, RefinementType } from "../api";
|
||||||
import { fetchModels, fetchSettings, refineText, getRefineErrorMessage, updateGlobalSettings } from "../api";
|
import { fetchModels, fetchSettings, refineText, getRefineErrorMessage, updateGlobalSettings } from "../api";
|
||||||
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, MoreHorizontal } from "lucide-react";
|
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight } from "lucide-react";
|
||||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||||
|
|
||||||
const STORAGE_KEY = "kb-quick-entry-text";
|
const STORAGE_KEY = "kb-quick-entry-text";
|
||||||
@@ -117,12 +117,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
const refineMenuPortalRef = useRef<HTMLDivElement>(null);
|
const refineMenuPortalRef = useRef<HTMLDivElement>(null);
|
||||||
const [refineMenuPosition, setRefineMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
const [refineMenuPosition, setRefineMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
// Actions dropdown state (Deps/Models/Save behind a single dropdown)
|
|
||||||
const [isActionsDropdownOpen, setIsActionsDropdownOpen] = useState(false);
|
|
||||||
const actionsDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const actionsDropdownPortalRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [actionsDropdownPosition, setActionsDropdownPosition] = useState<{ top: number; left: number; width: number } | null>(null);
|
|
||||||
|
|
||||||
// Use parent-provided favorites when available, otherwise internal state
|
// Use parent-provided favorites when available, otherwise internal state
|
||||||
const effectiveFavoriteProviders = parentFavoriteProviders ?? favoriteProviders;
|
const effectiveFavoriteProviders = parentFavoriteProviders ?? favoriteProviders;
|
||||||
const effectiveFavoriteModels = parentFavoriteModels ?? favoriteModels;
|
const effectiveFavoriteModels = parentFavoriteModels ?? favoriteModels;
|
||||||
@@ -323,7 +317,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
setIsRefining(false);
|
setIsRefining(false);
|
||||||
setIsExpanded(false); // Collapse textarea height on reset
|
setIsExpanded(false); // Collapse textarea height on reset
|
||||||
setIsDisclosureExpanded(false); // Always reset controls to collapsed after creation
|
setIsDisclosureExpanded(false); // Always reset controls to collapsed after creation
|
||||||
setIsActionsDropdownOpen(false); // Close actions dropdown on reset
|
|
||||||
justResetRef.current = true;
|
justResetRef.current = true;
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
textareaRef.current.style.height = "auto";
|
textareaRef.current.style.height = "auto";
|
||||||
@@ -403,11 +396,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
setIsRefineMenuOpen(false);
|
setIsRefineMenuOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Close actions dropdown if open
|
|
||||||
if (isActionsDropdownOpen) {
|
|
||||||
setIsActionsDropdownOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Clear non-empty input on Escape and clear localStorage
|
// Clear non-empty input on Escape and clear localStorage
|
||||||
if (description.trim()) {
|
if (description.trim()) {
|
||||||
setDescription("");
|
setDescription("");
|
||||||
@@ -434,7 +422,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
isModelMenuOpen,
|
isModelMenuOpen,
|
||||||
activeModelSubmenu,
|
activeModelSubmenu,
|
||||||
isRefineMenuOpen,
|
isRefineMenuOpen,
|
||||||
isActionsDropdownOpen,
|
|
||||||
setIsDisclosureExpanded,
|
setIsDisclosureExpanded,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -512,88 +499,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
});
|
});
|
||||||
}, [updateModelMenuPosition]);
|
}, [updateModelMenuPosition]);
|
||||||
|
|
||||||
const updateActionsDropdownPosition = useCallback(() => {
|
|
||||||
const trigger = actionsDropdownRef.current?.querySelector(".quick-entry-actions-trigger") as HTMLElement | null;
|
|
||||||
if (!trigger) return;
|
|
||||||
|
|
||||||
const rect = trigger.getBoundingClientRect();
|
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
const isMobile = viewportWidth <= 640;
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
const mobileWidth = Math.min(viewportWidth - 32, 320);
|
|
||||||
const left = Math.max((viewportWidth - mobileWidth) / 2, 16);
|
|
||||||
setActionsDropdownPosition({
|
|
||||||
top: rect.bottom + 4,
|
|
||||||
left,
|
|
||||||
width: mobileWidth,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setActionsDropdownPosition({
|
|
||||||
top: rect.bottom + 4,
|
|
||||||
left: rect.left,
|
|
||||||
width: Math.max(rect.width, 200),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleActionsDropdown = useCallback(() => {
|
|
||||||
setIsActionsDropdownOpen((prev) => {
|
|
||||||
const next = !prev;
|
|
||||||
if (next) {
|
|
||||||
updateActionsDropdownPosition();
|
|
||||||
} else {
|
|
||||||
setActionsDropdownPosition(null);
|
|
||||||
// Also close any child popovers
|
|
||||||
setShowDeps(false);
|
|
||||||
setIsModelMenuOpen(false);
|
|
||||||
setActiveModelSubmenu(null);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, [updateActionsDropdownPosition]);
|
|
||||||
|
|
||||||
// Close actions dropdown when clicking outside
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isActionsDropdownOpen) return;
|
|
||||||
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
const target = e.target as Node;
|
|
||||||
const clickedInsideTrigger = actionsDropdownRef.current?.contains(target);
|
|
||||||
const clickedInsidePortal = actionsDropdownPortalRef.current?.contains(target);
|
|
||||||
// Also check for clicks inside child popovers (model menu, dep dropdown)
|
|
||||||
const clickedInsideModelMenu = (target instanceof Element) && (
|
|
||||||
target.closest?.(".model-nested-menu--portal") != null ||
|
|
||||||
target.closest?.(".model-combobox-dropdown--portal") != null
|
|
||||||
);
|
|
||||||
const clickedInsideDepDropdown = (target instanceof Element) && (
|
|
||||||
target.closest?.(".dep-dropdown") != null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!clickedInsideTrigger && !clickedInsidePortal && !clickedInsideModelMenu && !clickedInsideDepDropdown) {
|
|
||||||
setIsActionsDropdownOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [isActionsDropdownOpen]);
|
|
||||||
|
|
||||||
// Keep actions dropdown portal anchored during scroll/resize
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isActionsDropdownOpen) return;
|
|
||||||
|
|
||||||
const handleReposition = () => updateActionsDropdownPosition();
|
|
||||||
|
|
||||||
window.addEventListener("resize", handleReposition);
|
|
||||||
window.addEventListener("scroll", handleReposition, true);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("resize", handleReposition);
|
|
||||||
window.removeEventListener("scroll", handleReposition, true);
|
|
||||||
};
|
|
||||||
}, [isActionsDropdownOpen, updateActionsDropdownPosition]);
|
|
||||||
|
|
||||||
const updateRefineMenuPosition = useCallback(() => {
|
const updateRefineMenuPosition = useCallback(() => {
|
||||||
const trigger = refineMenuRef.current?.querySelector(".refine-button") as HTMLElement | null;
|
const trigger = refineMenuRef.current?.querySelector(".refine-button") as HTMLElement | null;
|
||||||
if (!trigger) return;
|
if (!trigger) return;
|
||||||
@@ -933,249 +838,220 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
aria-hidden={!showExpandedControls}
|
aria-hidden={!showExpandedControls}
|
||||||
>
|
>
|
||||||
<div className="quick-entry-controls-left">
|
<div className="quick-entry-controls-left">
|
||||||
<div className="quick-entry-actions-wrap" ref={actionsDropdownRef}>
|
{/* Deps button */}
|
||||||
|
<div className="dep-trigger-wrap">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm quick-entry-actions-trigger"
|
className="btn btn-sm dep-trigger"
|
||||||
onClick={toggleActionsDropdown}
|
onClick={toggleDepsDropdown}
|
||||||
aria-expanded={isActionsDropdownOpen}
|
data-testid="quick-entry-deps-button"
|
||||||
aria-haspopup="menu"
|
|
||||||
data-testid="quick-entry-actions-button"
|
|
||||||
title="Dependencies, models, and save"
|
|
||||||
>
|
>
|
||||||
<MoreHorizontal size={12} style={{ verticalAlign: "middle" }} />
|
<Link size={12} style={{ verticalAlign: "middle" }} />
|
||||||
Actions
|
{dependencies.length > 0 ? ` ${dependencies.length} deps` : " Deps"}
|
||||||
</button>
|
</button>
|
||||||
{isActionsDropdownOpen && portalRoot && actionsDropdownPosition && createPortal(
|
{showDeps && (() => {
|
||||||
<div
|
const term = depSearch.toLowerCase();
|
||||||
ref={actionsDropdownPortalRef}
|
const filtered = (term
|
||||||
className="actions-dropdown actions-dropdown--portal"
|
? tasks.filter((t) =>
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
t.id.toLowerCase().includes(term) ||
|
||||||
data-testid="actions-dropdown"
|
(t.title && t.title.toLowerCase().includes(term)) ||
|
||||||
style={{
|
(t.description && t.description.toLowerCase().includes(term))
|
||||||
position: "fixed",
|
)
|
||||||
top: `${actionsDropdownPosition.top}px`,
|
: [...tasks]
|
||||||
left: `${actionsDropdownPosition.left}px`,
|
).sort((a, b) => {
|
||||||
width: `${actionsDropdownPosition.width}px`,
|
const cmp = b.createdAt.localeCompare(a.createdAt);
|
||||||
}}
|
if (cmp !== 0) return cmp;
|
||||||
>
|
const aNum = parseInt(a.id.slice(a.id.lastIndexOf("-") + 1), 10) || 0;
|
||||||
<div className="actions-dropdown-items">
|
const bNum = parseInt(b.id.slice(b.id.lastIndexOf("-") + 1), 10) || 0;
|
||||||
<div className="dep-trigger-wrap">
|
return bNum - aNum;
|
||||||
<button
|
});
|
||||||
type="button"
|
return (
|
||||||
className="btn btn-sm dep-trigger"
|
<div className="dep-dropdown" onMouseDown={(e) => e.preventDefault()}>
|
||||||
onClick={toggleDepsDropdown}
|
<input
|
||||||
data-testid="quick-entry-deps-button"
|
className="dep-dropdown-search"
|
||||||
>
|
placeholder="Search tasks…"
|
||||||
<Link size={12} style={{ verticalAlign: "middle" }} />
|
autoFocus
|
||||||
{dependencies.length > 0 ? ` ${dependencies.length} deps` : " Deps"}
|
value={depSearch}
|
||||||
</button>
|
onChange={(e) => setDepSearch(e.target.value)}
|
||||||
{showDeps && (() => {
|
onClick={(e) => e.stopPropagation()}
|
||||||
const term = depSearch.toLowerCase();
|
/>
|
||||||
const filtered = (term
|
{filtered.length === 0 ? (
|
||||||
? tasks.filter((t) =>
|
<div className="dep-dropdown-empty">No existing tasks</div>
|
||||||
t.id.toLowerCase().includes(term) ||
|
) : (
|
||||||
(t.title && t.title.toLowerCase().includes(term)) ||
|
filtered.map((t) => (
|
||||||
(t.description && t.description.toLowerCase().includes(term))
|
|
||||||
)
|
|
||||||
: [...tasks]
|
|
||||||
).sort((a, b) => {
|
|
||||||
const cmp = b.createdAt.localeCompare(a.createdAt);
|
|
||||||
if (cmp !== 0) return cmp;
|
|
||||||
const aNum = parseInt(a.id.slice(a.id.lastIndexOf("-") + 1), 10) || 0;
|
|
||||||
const bNum = parseInt(b.id.slice(b.id.lastIndexOf("-") + 1), 10) || 0;
|
|
||||||
return bNum - aNum;
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className="dep-dropdown" onMouseDown={(e) => e.preventDefault()}>
|
|
||||||
<input
|
|
||||||
className="dep-dropdown-search"
|
|
||||||
placeholder="Search tasks…"
|
|
||||||
autoFocus
|
|
||||||
value={depSearch}
|
|
||||||
onChange={(e) => setDepSearch(e.target.value)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
{filtered.length === 0 ? (
|
|
||||||
<div className="dep-dropdown-empty">No existing tasks</div>
|
|
||||||
) : (
|
|
||||||
filtered.map((t) => (
|
|
||||||
<div
|
|
||||||
key={t.id}
|
|
||||||
className={`dep-dropdown-item${dependencies.includes(t.id) ? " selected" : ""}`}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
onClick={() => toggleDep(t.id)}
|
|
||||||
>
|
|
||||||
<span className="dep-dropdown-id">{t.id}</span>
|
|
||||||
<span className="dep-dropdown-title">{truncate(t.title || t.description || t.id, 30)}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="quick-entry-model-wrap" ref={modelMenuRef}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm quick-entry-model-trigger"
|
|
||||||
onClick={toggleModelMenu}
|
|
||||||
aria-expanded={isModelMenuOpen}
|
|
||||||
aria-haspopup="menu"
|
|
||||||
data-testid="quick-entry-models-button"
|
|
||||||
>
|
|
||||||
<Brain size={12} style={{ verticalAlign: "middle" }} />
|
|
||||||
{selectedPreset
|
|
||||||
? ` ${selectedPreset.name}`
|
|
||||||
: selectedModelCount > 0
|
|
||||||
? ` ${selectedModelCount} model${selectedModelCount === 1 ? "" : "s"}`
|
|
||||||
: " Models"}
|
|
||||||
</button>
|
|
||||||
{isModelMenuOpen && portalRoot && modelMenuPosition && createPortal(
|
|
||||||
<div
|
<div
|
||||||
ref={modelMenuPortalRef}
|
key={t.id}
|
||||||
className="model-nested-menu model-nested-menu--portal"
|
className={`dep-dropdown-item${dependencies.includes(t.id) ? " selected" : ""}`}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
data-testid="model-nested-menu"
|
onClick={() => toggleDep(t.id)}
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: `${modelMenuPosition.top}px`,
|
|
||||||
left: `${modelMenuPosition.left}px`,
|
|
||||||
width: `${modelMenuPosition.width}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{activeModelSubmenu === null ? (
|
<span className="dep-dropdown-id">{t.id}</span>
|
||||||
// Top-level menu with Plan/Executor/Validator choices
|
<span className="dep-dropdown-title">{truncate(t.title || t.description || t.id, 30)}</span>
|
||||||
<div className="model-menu-items">
|
</div>
|
||||||
<button
|
))
|
||||||
type="button"
|
|
||||||
className={`model-menu-item ${hasPlanningOverride ? "model-menu-item--active" : ""}`}
|
|
||||||
onClick={() => setActiveModelSubmenu("plan")}
|
|
||||||
data-testid="model-menu-plan"
|
|
||||||
>
|
|
||||||
<span className="model-menu-item-label">
|
|
||||||
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
|
|
||||||
Plan
|
|
||||||
</span>
|
|
||||||
<span className="model-menu-item-value">
|
|
||||||
{hasPlanningOverride
|
|
||||||
? getModelBadgeLabel(planningProvider, planningModelId)
|
|
||||||
: "Using default"}
|
|
||||||
</span>
|
|
||||||
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`model-menu-item ${hasExecutorOverride ? "model-menu-item--active" : ""}`}
|
|
||||||
onClick={() => setActiveModelSubmenu("executor")}
|
|
||||||
data-testid="model-menu-executor"
|
|
||||||
>
|
|
||||||
<span className="model-menu-item-label">
|
|
||||||
<Sparkles size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
|
|
||||||
Executor
|
|
||||||
</span>
|
|
||||||
<span className="model-menu-item-value">
|
|
||||||
{hasExecutorOverride
|
|
||||||
? getModelBadgeLabel(executorProvider, executorModelId)
|
|
||||||
: "Using default"}
|
|
||||||
</span>
|
|
||||||
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`model-menu-item ${hasValidatorOverride ? "model-menu-item--active" : ""}`}
|
|
||||||
onClick={() => setActiveModelSubmenu("validator")}
|
|
||||||
data-testid="model-menu-validator"
|
|
||||||
>
|
|
||||||
<span className="model-menu-item-label">
|
|
||||||
<Brain size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
|
|
||||||
Validator
|
|
||||||
</span>
|
|
||||||
<span className="model-menu-item-value">
|
|
||||||
{hasValidatorOverride
|
|
||||||
? getModelBadgeLabel(validatorProvider, validatorModelId)
|
|
||||||
: "Using default"}
|
|
||||||
</span>
|
|
||||||
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// Submenu with CustomModelDropdown for the selected target
|
|
||||||
<div className="model-submenu">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="model-submenu-back"
|
|
||||||
onClick={() => setActiveModelSubmenu(null)}
|
|
||||||
data-testid="model-submenu-back"
|
|
||||||
>
|
|
||||||
<ChevronDown size={12} style={{ transform: "rotate(90deg)", marginRight: 4 }} />
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<div className="model-submenu-header">
|
|
||||||
{activeModelSubmenu === "plan" && "Plan Model"}
|
|
||||||
{activeModelSubmenu === "executor" && "Executor Model"}
|
|
||||||
{activeModelSubmenu === "validator" && "Validator Model"}
|
|
||||||
</div>
|
|
||||||
<CustomModelDropdown
|
|
||||||
models={loadedModels}
|
|
||||||
value={
|
|
||||||
activeModelSubmenu === "plan"
|
|
||||||
? planningSelectionValue
|
|
||||||
: activeModelSubmenu === "executor"
|
|
||||||
? executorSelectionValue
|
|
||||||
: validatorSelectionValue
|
|
||||||
}
|
|
||||||
onChange={
|
|
||||||
activeModelSubmenu === "plan"
|
|
||||||
? handlePlanningModelChange
|
|
||||||
: activeModelSubmenu === "executor"
|
|
||||||
? handleExecutorChange
|
|
||||||
: handleValidatorChange
|
|
||||||
}
|
|
||||||
placeholder="Using default"
|
|
||||||
disabled={modelsLoading}
|
|
||||||
id={`model-${activeModelSubmenu}-select`}
|
|
||||||
label={`${activeModelSubmenu} model`}
|
|
||||||
favoriteProviders={effectiveFavoriteProviders}
|
|
||||||
onToggleFavorite={handleToggleFavorite}
|
|
||||||
favoriteModels={effectiveFavoriteModels}
|
|
||||||
onToggleModelFavorite={handleToggleModelFavorite}
|
|
||||||
/>
|
|
||||||
{modelsError && (
|
|
||||||
<div className="model-submenu-error">
|
|
||||||
<span>{modelsError}</span>
|
|
||||||
<button type="button" className="btn btn-sm" onClick={loadModels}>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>,
|
|
||||||
portalRoot,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isSubmitting && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-task-create btn-sm"
|
|
||||||
onClick={handleSaveClick}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
disabled={!description.trim() || isSubmitting}
|
|
||||||
data-testid="save-button"
|
|
||||||
title="Create task"
|
|
||||||
>
|
|
||||||
<Save size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Models button */}
|
||||||
|
<div className="quick-entry-model-wrap" ref={modelMenuRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm quick-entry-model-trigger"
|
||||||
|
onClick={toggleModelMenu}
|
||||||
|
aria-expanded={isModelMenuOpen}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
data-testid="quick-entry-models-button"
|
||||||
|
>
|
||||||
|
<Brain size={12} style={{ verticalAlign: "middle" }} />
|
||||||
|
{selectedPreset
|
||||||
|
? ` ${selectedPreset.name}`
|
||||||
|
: selectedModelCount > 0
|
||||||
|
? ` ${selectedModelCount} model${selectedModelCount === 1 ? "" : "s"}`
|
||||||
|
: " Models"}
|
||||||
|
</button>
|
||||||
|
{isModelMenuOpen && portalRoot && modelMenuPosition && createPortal(
|
||||||
|
<div
|
||||||
|
ref={modelMenuPortalRef}
|
||||||
|
className="model-nested-menu model-nested-menu--portal"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
data-testid="model-nested-menu"
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: `${modelMenuPosition.top}px`,
|
||||||
|
left: `${modelMenuPosition.left}px`,
|
||||||
|
width: `${modelMenuPosition.width}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeModelSubmenu === null ? (
|
||||||
|
// Top-level menu with Plan/Executor/Validator choices
|
||||||
|
<div className="model-menu-items">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`model-menu-item ${hasPlanningOverride ? "model-menu-item--active" : ""}`}
|
||||||
|
onClick={() => setActiveModelSubmenu("plan")}
|
||||||
|
data-testid="model-menu-plan"
|
||||||
|
>
|
||||||
|
<span className="model-menu-item-label">
|
||||||
|
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
|
||||||
|
Plan
|
||||||
|
</span>
|
||||||
|
<span className="model-menu-item-value">
|
||||||
|
{hasPlanningOverride
|
||||||
|
? getModelBadgeLabel(planningProvider, planningModelId)
|
||||||
|
: "Using default"}
|
||||||
|
</span>
|
||||||
|
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`model-menu-item ${hasExecutorOverride ? "model-menu-item--active" : ""}`}
|
||||||
|
onClick={() => setActiveModelSubmenu("executor")}
|
||||||
|
data-testid="model-menu-executor"
|
||||||
|
>
|
||||||
|
<span className="model-menu-item-label">
|
||||||
|
<Sparkles size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
|
||||||
|
Executor
|
||||||
|
</span>
|
||||||
|
<span className="model-menu-item-value">
|
||||||
|
{hasExecutorOverride
|
||||||
|
? getModelBadgeLabel(executorProvider, executorModelId)
|
||||||
|
: "Using default"}
|
||||||
|
</span>
|
||||||
|
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`model-menu-item ${hasValidatorOverride ? "model-menu-item--active" : ""}`}
|
||||||
|
onClick={() => setActiveModelSubmenu("validator")}
|
||||||
|
data-testid="model-menu-validator"
|
||||||
|
>
|
||||||
|
<span className="model-menu-item-label">
|
||||||
|
<Brain size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
|
||||||
|
Validator
|
||||||
|
</span>
|
||||||
|
<span className="model-menu-item-value">
|
||||||
|
{hasValidatorOverride
|
||||||
|
? getModelBadgeLabel(validatorProvider, validatorModelId)
|
||||||
|
: "Using default"}
|
||||||
|
</span>
|
||||||
|
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Submenu with CustomModelDropdown for the selected target
|
||||||
|
<div className="model-submenu">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="model-submenu-back"
|
||||||
|
onClick={() => setActiveModelSubmenu(null)}
|
||||||
|
data-testid="model-submenu-back"
|
||||||
|
>
|
||||||
|
<ChevronDown size={12} style={{ transform: "rotate(90deg)", marginRight: 4 }} />
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<div className="model-submenu-header">
|
||||||
|
{activeModelSubmenu === "plan" && "Plan Model"}
|
||||||
|
{activeModelSubmenu === "executor" && "Executor Model"}
|
||||||
|
{activeModelSubmenu === "validator" && "Validator Model"}
|
||||||
|
</div>
|
||||||
|
<CustomModelDropdown
|
||||||
|
models={loadedModels}
|
||||||
|
value={
|
||||||
|
activeModelSubmenu === "plan"
|
||||||
|
? planningSelectionValue
|
||||||
|
: activeModelSubmenu === "executor"
|
||||||
|
? executorSelectionValue
|
||||||
|
: validatorSelectionValue
|
||||||
|
}
|
||||||
|
onChange={
|
||||||
|
activeModelSubmenu === "plan"
|
||||||
|
? handlePlanningModelChange
|
||||||
|
: activeModelSubmenu === "executor"
|
||||||
|
? handleExecutorChange
|
||||||
|
: handleValidatorChange
|
||||||
|
}
|
||||||
|
placeholder="Using default"
|
||||||
|
disabled={modelsLoading}
|
||||||
|
id={`model-${activeModelSubmenu}-select`}
|
||||||
|
label={`${activeModelSubmenu} model`}
|
||||||
|
favoriteProviders={effectiveFavoriteProviders}
|
||||||
|
onToggleFavorite={handleToggleFavorite}
|
||||||
|
favoriteModels={effectiveFavoriteModels}
|
||||||
|
onToggleModelFavorite={handleToggleModelFavorite}
|
||||||
|
/>
|
||||||
|
{modelsError && (
|
||||||
|
<div className="model-submenu-error">
|
||||||
|
<span>{modelsError}</span>
|
||||||
|
<button type="button" className="btn btn-sm" onClick={loadModels}>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>,
|
</div>,
|
||||||
portalRoot,
|
portalRoot,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Save button */}
|
||||||
|
{!isSubmitting && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-task-create btn-sm"
|
||||||
|
onClick={handleSaveClick}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
disabled={!description.trim() || isSubmitting}
|
||||||
|
data-testid="save-button"
|
||||||
|
title="Create task"
|
||||||
|
>
|
||||||
|
<Save size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="quick-entry-hint">
|
<div className="quick-entry-hint">
|
||||||
Enter to create · Esc to cancel
|
Enter to create · Esc to cancel
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ vi.mock("lucide-react", () => ({
|
|||||||
ChevronDown: () => null,
|
ChevronDown: () => null,
|
||||||
ChevronUp: () => null,
|
ChevronUp: () => null,
|
||||||
ChevronRight: () => null,
|
ChevronRight: () => null,
|
||||||
MoreHorizontal: () => null,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock ModelSelectionModal (kept for backward compatibility - no longer directly rendered)
|
// Mock ModelSelectionModal (kept for backward compatibility - no longer directly rendered)
|
||||||
@@ -161,12 +160,6 @@ function expandQuickEntry() {
|
|||||||
fireEvent.click(toggleButton);
|
fireEvent.click(toggleButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to open the actions dropdown (which contains Deps/Models/Save)
|
|
||||||
function openActionsDropdown() {
|
|
||||||
const actionsButton = screen.getByTestId("quick-entry-actions-button");
|
|
||||||
fireEvent.click(actionsButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("QuickEntryBox", () => {
|
describe("QuickEntryBox", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||||
@@ -507,7 +500,7 @@ describe("QuickEntryBox", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Rich creation features", () => {
|
describe("Rich creation features", () => {
|
||||||
it("shows dependency button when expanded and actions dropdown is open", () => {
|
it("shows dependency button when expanded", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
|
|
||||||
// Initially, controls region is collapsed/hidden
|
// Initially, controls region is collapsed/hidden
|
||||||
@@ -518,14 +511,11 @@ describe("QuickEntryBox", () => {
|
|||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
||||||
|
|
||||||
// Open the actions dropdown to access Deps/Models/Save
|
// The dependency button should be directly visible in the disclosure panel
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
// Now the dependency button should be visible
|
|
||||||
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows model selector button when expanded and actions dropdown is open", () => {
|
it("shows model selector button when expanded", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
|
|
||||||
// Initially, controls region is collapsed/hidden
|
// Initially, controls region is collapsed/hidden
|
||||||
@@ -536,10 +526,7 @@ describe("QuickEntryBox", () => {
|
|||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
|
|
||||||
// Open the actions dropdown to access Deps/Models/Save
|
// The model selector button should be directly visible in the disclosure panel
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
// Now the model selector button should be visible
|
|
||||||
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -562,7 +549,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("opens dependency dropdown when clicking deps button", () => {
|
it("opens dependency dropdown when clicking deps button", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
||||||
@@ -576,7 +562,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("opens model menu when clicking models button", () => {
|
it("opens model menu when clicking models button", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -594,7 +579,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("shows Plan, Executor, and Validator options in model menu", () => {
|
it("shows Plan, Executor, and Validator options in model menu", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -608,7 +592,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("clicking Executor opens submenu with CustomModelDropdown", () => {
|
it("clicking Executor opens submenu with CustomModelDropdown", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -624,7 +607,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("clicking Plan opens submenu with CustomModelDropdown", () => {
|
it("clicking Plan opens submenu with CustomModelDropdown", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -637,7 +619,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("clicking Validator opens submenu with CustomModelDropdown", () => {
|
it("clicking Validator opens submenu with CustomModelDropdown", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -650,7 +631,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("back button returns to top-level model menu", () => {
|
it("back button returns to top-level model menu", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -669,7 +649,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("Escape from submenu returns to top-level menu without closing it", () => {
|
it("Escape from submenu returns to top-level menu without closing it", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -693,7 +672,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("selecting Plan model updates the Plan menu item value", () => {
|
it("selecting Plan model updates the Plan menu item value", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -717,7 +695,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("selecting Validator model updates the Validator menu item value", () => {
|
it("selecting Validator model updates the Validator menu item value", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -739,7 +716,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("clearing Plan model returns menu item to default state", () => {
|
it("clearing Plan model returns menu item to default state", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||||
@@ -762,7 +738,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("selects dependencies and includes them in submit payload", async () => {
|
it("selects dependencies and includes them in submit payload", async () => {
|
||||||
const { props } = renderQuickEntryBox({});
|
const { props } = renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
||||||
@@ -905,7 +880,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("includes selected models in submit payload", async () => {
|
it("includes selected models in submit payload", async () => {
|
||||||
const { props } = renderQuickEntryBox({});
|
const { props } = renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with model" } });
|
fireEvent.change(textarea, { target: { value: "Task with model" } });
|
||||||
@@ -940,7 +914,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("closes model menu on Escape when open", async () => {
|
it("closes model menu on Escape when open", async () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with menu" } });
|
fireEvent.change(textarea, { target: { value: "Task with menu" } });
|
||||||
@@ -959,20 +932,32 @@ describe("QuickEntryBox", () => {
|
|||||||
expect((textarea as HTMLTextAreaElement).value).toBe("Task with menu");
|
expect((textarea as HTMLTextAreaElement).value).toBe("Task with menu");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears all state on second Escape after dropdowns are closed", () => {
|
it("Escape hierarchy: model submenu → model menu → deps popover → input clear", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task to clear" } });
|
fireEvent.change(textarea, { target: { value: "Hierarchy test" } });
|
||||||
|
|
||||||
// First Escape closes any dropdowns
|
// Open model menu
|
||||||
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
|
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||||
|
|
||||||
|
// Open executor submenu
|
||||||
|
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||||
|
expect(screen.getByTestId("model-submenu-back")).toBeTruthy();
|
||||||
|
|
||||||
|
// Escape 1: close submenu → back to model menu top level
|
||||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||||
|
expect(screen.queryByTestId("model-submenu-back")).toBeNull();
|
||||||
|
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||||
|
|
||||||
// Second Escape clears everything
|
// Escape 2: close model menu
|
||||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||||
|
expect(screen.queryByTestId("model-nested-menu")).toBeNull();
|
||||||
|
|
||||||
// Input should be cleared and collapsed
|
// Escape 3: clear input and collapse
|
||||||
|
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||||
expect((textarea as HTMLTextAreaElement).value).toBe("");
|
expect((textarea as HTMLTextAreaElement).value).toBe("");
|
||||||
expect(textarea.classList.contains("quick-entry-input--expanded")).toBe(false);
|
expect(textarea.classList.contains("quick-entry-input--expanded")).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -1238,7 +1223,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("does not clear localStorage on first Escape when closing dropdowns", () => {
|
it("does not clear localStorage on first Escape when closing dropdowns", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
// Type something and open dropdown
|
// Type something and open dropdown
|
||||||
@@ -1493,7 +1477,7 @@ describe("QuickEntryBox", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Save button", () => {
|
describe("Save button", () => {
|
||||||
it("shows save button when expanded and actions dropdown is open", () => {
|
it("shows save button when expanded", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
|
|
||||||
// Initially, controls region is collapsed/hidden
|
// Initially, controls region is collapsed/hidden
|
||||||
@@ -1504,17 +1488,13 @@ describe("QuickEntryBox", () => {
|
|||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||||
|
|
||||||
// Open actions dropdown to access Save
|
// The save button should be directly visible in the disclosure panel
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
// Now the save button should be visible
|
|
||||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("save button is disabled when textarea is empty", () => {
|
it("save button is disabled when textarea is empty", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
// Type something
|
// Type something
|
||||||
@@ -1537,7 +1517,6 @@ describe("QuickEntryBox", () => {
|
|||||||
props.onCreate.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
|
props.onCreate.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
|
||||||
|
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "New task" } });
|
fireEvent.change(textarea, { target: { value: "New task" } });
|
||||||
@@ -1557,7 +1536,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("clicking save button persists to localStorage", async () => {
|
it("clicking save button persists to localStorage", async () => {
|
||||||
const { props } = renderQuickEntryBox({});
|
const { props } = renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Draft task description" } });
|
fireEvent.change(textarea, { target: { value: "Draft task description" } });
|
||||||
@@ -1580,7 +1558,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("clicking save button creates the task", async () => {
|
it("clicking save button creates the task", async () => {
|
||||||
const { props } = renderQuickEntryBox({});
|
const { props } = renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||||
@@ -1602,7 +1579,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("save button has correct test id", () => {
|
it("save button has correct test id", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||||
@@ -1617,7 +1593,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("save button has correct title attribute", () => {
|
it("save button has correct title attribute", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||||
@@ -1629,7 +1604,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("save button prevents textarea blur on mousedown", () => {
|
it("save button prevents textarea blur on mousedown", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||||
@@ -1676,8 +1650,10 @@ describe("QuickEntryBox", () => {
|
|||||||
|
|
||||||
// Now controls should be visible
|
// Now controls should be visible
|
||||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||||
// Actions button should be accessible (Deps/Models/Save are inside it)
|
// Deps/Models/Save should be directly accessible (no actions dropdown needed)
|
||||||
expect(screen.getByTestId("quick-entry-actions-button")).toBeTruthy();
|
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 always visible in description-actions when expanded
|
||||||
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
||||||
expect(screen.getByTestId("subtask-button")).toBeTruthy();
|
expect(screen.getByTestId("subtask-button")).toBeTruthy();
|
||||||
@@ -1689,7 +1665,7 @@ describe("QuickEntryBox", () => {
|
|||||||
// Expand
|
// Expand
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||||
expect(screen.getByTestId("quick-entry-actions-button")).toBeTruthy();
|
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
||||||
|
|
||||||
// Collapse
|
// Collapse
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
@@ -1698,12 +1674,11 @@ describe("QuickEntryBox", () => {
|
|||||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("verifies all buttons are accessible when expanded and actions dropdown is open", () => {
|
it("verifies all buttons are accessible when expanded", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
// All buttons should be accessible when expanded + actions dropdown open
|
// All buttons should be directly accessible when expanded (no actions dropdown needed)
|
||||||
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
||||||
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
||||||
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
||||||
@@ -1735,28 +1710,30 @@ describe("QuickEntryBox", () => {
|
|||||||
expect(screen.queryByTestId("quick-entry-description-actions")).toBeNull();
|
expect(screen.queryByTestId("quick-entry-description-actions")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Save button is in the actions dropdown, not description-actions", () => {
|
it("Save button is in the controls panel, not description-actions", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
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-description-actions");
|
||||||
const saveButton = screen.getByTestId("save-button");
|
const saveButton = screen.getByTestId("save-button");
|
||||||
|
|
||||||
// Save button should NOT be in the description-actions area (it's in the actions dropdown)
|
// Save button should NOT be in the description-actions area (it's in the controls panel)
|
||||||
expect(actionsContainer.contains(saveButton)).toBe(false);
|
expect(actionsContainer.contains(saveButton)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Deps and Models buttons are in the actions dropdown, not description-actions", () => {
|
it("Deps and Models buttons are in the controls panel, not description-actions", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
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-description-actions");
|
||||||
const depsButton = screen.getByTestId("quick-entry-deps-button");
|
const depsButton = screen.getByTestId("quick-entry-deps-button");
|
||||||
const modelsButton = screen.getByTestId("quick-entry-models-button");
|
const modelsButton = screen.getByTestId("quick-entry-models-button");
|
||||||
|
|
||||||
// Deps and Models should NOT be in the description-actions area (they're in the actions dropdown)
|
// Deps and Models should NOT be in the description-actions area (they're in the controls panel)
|
||||||
expect(actionsContainer.contains(depsButton)).toBe(false);
|
expect(actionsContainer.contains(depsButton)).toBe(false);
|
||||||
expect(actionsContainer.contains(modelsButton)).toBe(false);
|
expect(actionsContainer.contains(modelsButton)).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -1794,7 +1771,6 @@ describe("QuickEntryBox", () => {
|
|||||||
// Don't pass availableModels so component fetches settings itself
|
// Don't pass availableModels so component fetches settings itself
|
||||||
renderQuickEntryBox({ availableModels: undefined });
|
renderQuickEntryBox({ availableModels: undefined });
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
// Open model menu
|
// Open model menu
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
@@ -1827,7 +1803,6 @@ describe("QuickEntryBox", () => {
|
|||||||
const onCreate = vi.fn().mockResolvedValue(undefined);
|
const onCreate = vi.fn().mockResolvedValue(undefined);
|
||||||
renderQuickEntryBox({ onCreate, availableModels: undefined });
|
renderQuickEntryBox({ onCreate, availableModels: undefined });
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
// Open model menu and select an executor via submenu
|
// Open model menu and select an executor via submenu
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
@@ -1882,7 +1857,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("renders model menu as a portal in document.body (not inside QuickEntryBox)", () => {
|
it("renders model menu as a portal in document.body (not inside QuickEntryBox)", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
|
|
||||||
@@ -1901,7 +1875,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("positions the portaled menu with fixed positioning to escape column overflow", () => {
|
it("positions the portaled menu with fixed positioning to escape column overflow", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
|
|
||||||
@@ -1919,7 +1892,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("does not close model menu when clicking inside CustomModelDropdown portal", () => {
|
it("does not close model menu when clicking inside CustomModelDropdown portal", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||||
@@ -1953,7 +1925,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("does not close model menu when clicking inside the model-nested-menu portal itself", () => {
|
it("does not close model menu when clicking inside the model-nested-menu portal itself", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
const menu = screen.getByTestId("model-nested-menu");
|
const menu = screen.getByTestId("model-nested-menu");
|
||||||
@@ -1969,7 +1940,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("closes model menu on outside click (click outside both trigger and portal)", () => {
|
it("closes model menu on outside click (click outside both trigger and portal)", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
const textarea = screen.getByTestId("quick-entry-input");
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task with menu" } });
|
fireEvent.change(textarea, { target: { value: "Task with menu" } });
|
||||||
@@ -1992,7 +1962,6 @@ describe("QuickEntryBox", () => {
|
|||||||
it("repositions portaled menu on window resize while open", () => {
|
it("repositions portaled menu on window resize while open", () => {
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
const menu = screen.getByTestId("model-nested-menu");
|
const menu = screen.getByTestId("model-nested-menu");
|
||||||
@@ -2014,7 +1983,6 @@ describe("QuickEntryBox", () => {
|
|||||||
|
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
const menu = screen.getByTestId("model-nested-menu");
|
const menu = screen.getByTestId("model-nested-menu");
|
||||||
@@ -2029,7 +1997,6 @@ describe("QuickEntryBox", () => {
|
|||||||
|
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
const menu = screen.getByTestId("model-nested-menu");
|
const menu = screen.getByTestId("model-nested-menu");
|
||||||
@@ -2045,7 +2012,6 @@ describe("QuickEntryBox", () => {
|
|||||||
|
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
const menu = screen.getByTestId("model-nested-menu");
|
const menu = screen.getByTestId("model-nested-menu");
|
||||||
@@ -2063,7 +2029,6 @@ describe("QuickEntryBox", () => {
|
|||||||
|
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
const menu = screen.getByTestId("model-nested-menu");
|
const menu = screen.getByTestId("model-nested-menu");
|
||||||
@@ -2079,7 +2044,6 @@ describe("QuickEntryBox", () => {
|
|||||||
|
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
expandQuickEntry();
|
expandQuickEntry();
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||||
const menu = screen.getByTestId("model-nested-menu");
|
const menu = screen.getByTestId("model-nested-menu");
|
||||||
@@ -2098,185 +2062,4 @@ describe("QuickEntryBox", () => {
|
|||||||
expect(mobileWidth).toBeGreaterThan(desktopWidth);
|
expect(mobileWidth).toBeGreaterThan(desktopWidth);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Actions dropdown (FN-888)", () => {
|
|
||||||
it("shows actions trigger button when expanded", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
|
|
||||||
// Actions trigger should be visible in controls area
|
|
||||||
expect(screen.getByTestId("quick-entry-actions-button")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not show Deps/Models/Save directly when expanded without opening actions dropdown", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
|
|
||||||
// These should NOT be visible until actions dropdown is opened
|
|
||||||
expect(screen.queryByTestId("quick-entry-deps-button")).toBeNull();
|
|
||||||
expect(screen.queryByTestId("quick-entry-models-button")).toBeNull();
|
|
||||||
expect(screen.queryByTestId("save-button")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens actions dropdown on click and shows Deps/Models/Save", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
|
|
||||||
// Click the actions trigger
|
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
// Actions dropdown should be visible
|
|
||||||
expect(screen.getByTestId("actions-dropdown")).toBeTruthy();
|
|
||||||
// Deps, Models, Save should now be accessible
|
|
||||||
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
|
||||||
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
|
||||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes actions dropdown on second click", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
|
|
||||||
// Open
|
|
||||||
openActionsDropdown();
|
|
||||||
expect(screen.getByTestId("actions-dropdown")).toBeTruthy();
|
|
||||||
|
|
||||||
// Close by clicking trigger again
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-actions-button"));
|
|
||||||
expect(screen.queryByTestId("actions-dropdown")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes actions dropdown on Escape (after child popovers are closed)", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Test" } });
|
|
||||||
|
|
||||||
// Actions dropdown should be open
|
|
||||||
expect(screen.getByTestId("actions-dropdown")).toBeTruthy();
|
|
||||||
|
|
||||||
// Press Escape — should close actions dropdown
|
|
||||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
|
||||||
|
|
||||||
// Actions dropdown should be closed
|
|
||||||
expect(screen.queryByTestId("actions-dropdown")).toBeNull();
|
|
||||||
// Input should still have the value
|
|
||||||
expect((textarea as HTMLTextAreaElement).value).toBe("Test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Escape hierarchy: model submenu → model menu → deps popover → actions dropdown → input clear", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
openActionsDropdown();
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
|
||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: "Hierarchy test" } });
|
|
||||||
|
|
||||||
// Open model menu
|
|
||||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
|
||||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
|
||||||
|
|
||||||
// Open executor submenu
|
|
||||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
|
||||||
expect(screen.getByTestId("model-submenu-back")).toBeTruthy();
|
|
||||||
|
|
||||||
// Escape 1: close submenu → back to model menu top level
|
|
||||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
|
||||||
expect(screen.queryByTestId("model-submenu-back")).toBeNull();
|
|
||||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
|
||||||
|
|
||||||
// Escape 2: close model menu
|
|
||||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
|
||||||
expect(screen.queryByTestId("model-nested-menu")).toBeNull();
|
|
||||||
expect(screen.getByTestId("actions-dropdown")).toBeTruthy();
|
|
||||||
|
|
||||||
// Escape 3: close actions dropdown
|
|
||||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
|
||||||
expect(screen.queryByTestId("actions-dropdown")).toBeNull();
|
|
||||||
|
|
||||||
// Escape 4: clear input and collapse
|
|
||||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
|
||||||
expect((textarea as HTMLTextAreaElement).value).toBe("");
|
|
||||||
expect(textarea.classList.contains("quick-entry-input--expanded")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("actions dropdown has proper aria attributes on trigger", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
|
|
||||||
const trigger = screen.getByTestId("quick-entry-actions-button");
|
|
||||||
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
||||||
expect(trigger.getAttribute("aria-haspopup")).toBe("menu");
|
|
||||||
|
|
||||||
// Open
|
|
||||||
openActionsDropdown();
|
|
||||||
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders actions dropdown as a portal in document.body", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
const dropdown = screen.getByTestId("actions-dropdown");
|
|
||||||
expect(dropdown).toBeTruthy();
|
|
||||||
|
|
||||||
// The dropdown should be in document.body, NOT inside the QuickEntryBox
|
|
||||||
const quickEntryBox = screen.getByTestId("quick-entry-box");
|
|
||||||
expect(quickEntryBox.contains(dropdown)).toBe(false);
|
|
||||||
expect(document.body.contains(dropdown)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("positions the actions dropdown with fixed positioning", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
const dropdown = screen.getByTestId("actions-dropdown");
|
|
||||||
expect(dropdown.style.position).toBe("fixed");
|
|
||||||
expect(dropdown.style.top).toBeTruthy();
|
|
||||||
expect(dropdown.style.left).toBeTruthy();
|
|
||||||
expect(dropdown.style.width).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes actions dropdown when clicking outside", () => {
|
|
||||||
renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
expect(screen.getByTestId("actions-dropdown")).toBeTruthy();
|
|
||||||
|
|
||||||
// Click outside
|
|
||||||
const outsideElement = document.createElement("div");
|
|
||||||
document.body.appendChild(outsideElement);
|
|
||||||
try {
|
|
||||||
fireEvent.mouseDown(outsideElement);
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(outsideElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(screen.queryByTestId("actions-dropdown")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets actions dropdown state on form reset after creation", async () => {
|
|
||||||
const { props } = renderQuickEntryBox({});
|
|
||||||
expandQuickEntry();
|
|
||||||
openActionsDropdown();
|
|
||||||
|
|
||||||
const textarea = screen.getByTestId("quick-entry-input");
|
|
||||||
fireEvent.change(textarea, { target: { value: "Task to create" } });
|
|
||||||
|
|
||||||
// Submit
|
|
||||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(props.onCreate).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
// After creation, actions dropdown should be closed
|
|
||||||
expect(screen.queryByTestId("actions-dropdown")).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user