feat(FN-1088): move quick entry advanced controls into actions menu
- Replace direct Deps/Models/Save controls with a single actions trigger in QuickEntryBox - Add portaled actions dropdown behavior, including outside-click and Escape handling plus menu repositioning - Add actions badge styling/count to reflect selected dependencies and model overrides - Update QuickEntryBox/ListView tests and AGENTS.md docs to match the new actions-menu interaction flow
This commit is contained in:
@@ -726,7 +726,7 @@ Both components provide the same task creation experience with the following opt
|
||||
- Both Plan and Subtask buttons are disabled when no description is entered.
|
||||
- Clicking either button clears the input after triggering the action.
|
||||
- Regular task creation (Enter key) works as before without AI assistance.
|
||||
- The actions dropdown closes on Escape or outside click. Escape dismisses in order: model submenu → model menu → dependency picker → actions dropdown → input clear/collapse.
|
||||
- The actions dropdown closes on Escape or outside click. Escape dismisses in order: actions dropdown → model submenu → model menu → dependency picker → input clear/collapse.
|
||||
|
||||
### Subtask Breakdown Dialog
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ToastType } from "../hooks/useToast";
|
||||
import type { Task, TaskCreateInput, Settings } from "@fusion/core";
|
||||
import type { ModelInfo, RefinementType } from "../api";
|
||||
import { fetchModels, fetchSettings, refineText, getRefineErrorMessage, updateGlobalSettings } from "../api";
|
||||
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight } from "lucide-react";
|
||||
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save, MoreHorizontal, ChevronDown, ChevronUp, ChevronRight } from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
|
||||
const STORAGE_KEY = "kb-quick-entry-text";
|
||||
@@ -90,6 +90,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const [dependencies, setDependencies] = useState<string[]>([]);
|
||||
const [showDeps, setShowDeps] = useState(false);
|
||||
const [depSearch, setDepSearch] = useState("");
|
||||
const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false);
|
||||
const [actionsMenuPosition, setActionsMenuPosition] = useState<{ top: number; left: number; width: number } | null>(null);
|
||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||
const [activeModelSubmenu, setActiveModelSubmenu] = useState<"plan" | "executor" | "validator" | null>(null);
|
||||
const [executorProvider, setExecutorProvider] = useState<string | undefined>(undefined);
|
||||
@@ -98,6 +100,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const [validatorModelId, setValidatorModelId] = useState<string | undefined>(undefined);
|
||||
const [planningProvider, setPlanningProvider] = useState<string | undefined>(undefined);
|
||||
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
|
||||
const actionsMenuRef = useRef<HTMLDivElement>(null);
|
||||
const actionsMenuPortalRef = useRef<HTMLDivElement>(null);
|
||||
const modelMenuRef = useRef<HTMLDivElement>(null);
|
||||
const modelMenuPortalRef = useRef<HTMLDivElement>(null);
|
||||
const [modelMenuPosition, setModelMenuPosition] = useState<{ top: number; left: number; width: number } | null>(null);
|
||||
@@ -183,13 +187,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const hasValidatorOverride = Boolean(validatorProvider && validatorModelId);
|
||||
const hasPlanningOverride = Boolean(planningProvider && planningModelId);
|
||||
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride);
|
||||
|
||||
const availablePresets = settings?.modelPresets || [];
|
||||
const selectedPreset = availablePresets.find((p) => p.id === selectedPresetId);
|
||||
|
||||
const handlePresetChange = useCallback((presetId: string | undefined) => {
|
||||
setSelectedPresetId(presetId);
|
||||
}, []);
|
||||
const modelMenuLabel = selectedPresetId
|
||||
? settings?.modelPresets?.find((p) => p.id === selectedPresetId)?.name ?? "Models"
|
||||
: selectedModelCount > 0
|
||||
? `${selectedModelCount} model${selectedModelCount === 1 ? "" : "s"}`
|
||||
: "Models";
|
||||
const actionSelectionCount = dependencies.length + selectedModelCount;
|
||||
|
||||
const getModelBadgeLabel = useCallback(
|
||||
(provider?: string, modelId?: string) => {
|
||||
@@ -279,6 +282,27 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isRefineMenuOpen]);
|
||||
|
||||
// Close actions menu when clicking outside
|
||||
useEffect(() => {
|
||||
if (!isActionsMenuOpen) return;
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
const clickedInsideTrigger = actionsMenuRef.current?.contains(target);
|
||||
const clickedInsidePortal = actionsMenuPortalRef.current?.contains(target);
|
||||
// Also check for clicks inside CustomModelDropdown's portaled dropdown
|
||||
const clickedInsideCombobox = (target instanceof Element) && (target.closest?.(".model-combobox-dropdown--portal") != null);
|
||||
|
||||
if (!clickedInsideTrigger && !clickedInsidePortal && !clickedInsideCombobox) {
|
||||
setIsActionsMenuOpen(false);
|
||||
setActionsMenuPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isActionsMenuOpen]);
|
||||
|
||||
// Close model menu when clicking outside
|
||||
useEffect(() => {
|
||||
if (!isModelMenuOpen) return;
|
||||
@@ -311,7 +335,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
setPlanningModelId(undefined);
|
||||
setSelectedPresetId(undefined);
|
||||
setShowDeps(false);
|
||||
setIsActionsMenuOpen(false);
|
||||
setActionsMenuPosition(null);
|
||||
setIsModelMenuOpen(false);
|
||||
setModelMenuPosition(null);
|
||||
setActiveModelSubmenu(null);
|
||||
setIsRefineMenuOpen(false);
|
||||
setIsRefining(false);
|
||||
@@ -385,6 +412,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
handleSubmit();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
// Close actions menu first if open
|
||||
if (isActionsMenuOpen) {
|
||||
setIsActionsMenuOpen(false);
|
||||
setActionsMenuPosition(null);
|
||||
return;
|
||||
}
|
||||
// Close model submenu first if open
|
||||
if (activeModelSubmenu) {
|
||||
setActiveModelSubmenu(null);
|
||||
@@ -393,6 +426,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// Close model menu if open
|
||||
if (isModelMenuOpen) {
|
||||
setIsModelMenuOpen(false);
|
||||
setModelMenuPosition(null);
|
||||
return;
|
||||
}
|
||||
// Close dependency or refine popover if open
|
||||
@@ -424,6 +458,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
description,
|
||||
isExpanded,
|
||||
showDeps,
|
||||
isActionsMenuOpen,
|
||||
isModelMenuOpen,
|
||||
activeModelSubmenu,
|
||||
isRefineMenuOpen,
|
||||
@@ -452,19 +487,49 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const toggleDepsDropdown = useCallback(() => {
|
||||
setShowDeps((prev) => {
|
||||
const updateActionsMenuPosition = useCallback(() => {
|
||||
const trigger = actionsMenuRef.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);
|
||||
setActionsMenuPosition({
|
||||
top: rect.bottom + 4,
|
||||
left,
|
||||
width: mobileWidth,
|
||||
});
|
||||
} else {
|
||||
setActionsMenuPosition({
|
||||
top: rect.bottom + 4,
|
||||
left: rect.left,
|
||||
width: Math.max(rect.width, 200),
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleActionsMenu = useCallback(() => {
|
||||
setIsActionsMenuOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
setShowDeps(false);
|
||||
setIsModelMenuOpen(false);
|
||||
setModelMenuPosition(null);
|
||||
setActiveModelSubmenu(null);
|
||||
updateActionsMenuPosition();
|
||||
} else {
|
||||
setActionsMenuPosition(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
}, [updateActionsMenuPosition]);
|
||||
|
||||
const updateModelMenuPosition = useCallback(() => {
|
||||
const trigger = modelMenuRef.current?.querySelector(".quick-entry-model-trigger") as HTMLElement | null;
|
||||
const trigger = modelMenuRef.current?.querySelector(".quick-entry-actions-trigger") as HTMLElement | null;
|
||||
if (!trigger) return;
|
||||
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
@@ -489,21 +554,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleModelMenu = useCallback(() => {
|
||||
setIsModelMenuOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
setShowDeps(false);
|
||||
// Compute position synchronously so the portal renders on first paint
|
||||
updateModelMenuPosition();
|
||||
} else {
|
||||
setActiveModelSubmenu(null);
|
||||
setModelMenuPosition(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [updateModelMenuPosition]);
|
||||
|
||||
const updateRefineMenuPosition = useCallback(() => {
|
||||
const trigger = refineMenuRef.current?.querySelector(".refine-button") as HTMLElement | null;
|
||||
if (!trigger) return;
|
||||
@@ -522,6 +572,21 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Keep actions menu portal anchored during scroll/resize
|
||||
useEffect(() => {
|
||||
if (!isActionsMenuOpen) return;
|
||||
|
||||
const handleReposition = () => updateActionsMenuPosition();
|
||||
|
||||
window.addEventListener("resize", handleReposition);
|
||||
window.addEventListener("scroll", handleReposition, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleReposition);
|
||||
window.removeEventListener("scroll", handleReposition, true);
|
||||
};
|
||||
}, [isActionsMenuOpen, updateActionsMenuPosition]);
|
||||
|
||||
// Keep model menu portal anchored during scroll/resize
|
||||
useEffect(() => {
|
||||
if (!isModelMenuOpen) return;
|
||||
@@ -843,17 +908,96 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
</div>
|
||||
)}
|
||||
<div className="quick-entry-controls-left">
|
||||
{/* Deps button */}
|
||||
<div className="dep-trigger-wrap">
|
||||
<div
|
||||
className="quick-entry-actions-wrap dep-trigger-wrap"
|
||||
ref={(node) => {
|
||||
actionsMenuRef.current = node;
|
||||
modelMenuRef.current = node;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm dep-trigger"
|
||||
onClick={toggleDepsDropdown}
|
||||
data-testid="quick-entry-deps-button"
|
||||
className="btn btn-sm quick-entry-actions-trigger"
|
||||
onClick={toggleActionsMenu}
|
||||
aria-expanded={isActionsMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
aria-label="Advanced creation options"
|
||||
data-testid="quick-entry-actions-trigger"
|
||||
>
|
||||
<Link size={12} style={{ verticalAlign: "middle" }} />
|
||||
{dependencies.length > 0 ? ` ${dependencies.length} deps` : " Deps"}
|
||||
<MoreHorizontal size={12} style={{ verticalAlign: "middle" }} />
|
||||
{actionSelectionCount > 0 && (
|
||||
<span className="quick-entry-actions-badge" data-testid="quick-entry-actions-badge">
|
||||
{actionSelectionCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isActionsMenuOpen && portalRoot && actionsMenuPosition && createPortal(
|
||||
<div
|
||||
ref={actionsMenuPortalRef}
|
||||
className="actions-dropdown actions-dropdown--portal"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: `${actionsMenuPosition.top}px`,
|
||||
left: `${actionsMenuPosition.left}px`,
|
||||
width: `${actionsMenuPosition.width}px`,
|
||||
}}
|
||||
>
|
||||
<div className="actions-dropdown-items">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm dep-trigger"
|
||||
data-testid="quick-entry-actions-deps"
|
||||
onClick={() => {
|
||||
setIsActionsMenuOpen(false);
|
||||
setActionsMenuPosition(null);
|
||||
setIsModelMenuOpen(false);
|
||||
setModelMenuPosition(null);
|
||||
setActiveModelSubmenu(null);
|
||||
setShowDeps(true);
|
||||
}}
|
||||
>
|
||||
<Link size={12} style={{ verticalAlign: "middle" }} />
|
||||
{dependencies.length > 0 ? `${dependencies.length} deps` : "Deps"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
data-testid="quick-entry-actions-models"
|
||||
onClick={() => {
|
||||
setIsActionsMenuOpen(false);
|
||||
setActionsMenuPosition(null);
|
||||
setShowDeps(false);
|
||||
setActiveModelSubmenu(null);
|
||||
setIsModelMenuOpen(true);
|
||||
updateModelMenuPosition();
|
||||
}}
|
||||
>
|
||||
<Brain size={12} style={{ verticalAlign: "middle" }} />
|
||||
{modelMenuLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-task-create btn-sm"
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
setIsActionsMenuOpen(false);
|
||||
setActionsMenuPosition(null);
|
||||
}}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
disabled={!description.trim() || isSubmitting}
|
||||
data-testid="quick-entry-actions-save"
|
||||
title="Create task"
|
||||
>
|
||||
<Save size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
portalRoot,
|
||||
)}
|
||||
|
||||
{showDeps && (() => {
|
||||
const term = depSearch.toLowerCase();
|
||||
const filtered = (term
|
||||
@@ -900,162 +1044,128 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
})()}
|
||||
</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>,
|
||||
portalRoot,
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Save button */}
|
||||
{!isSubmitting && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-task-create btn-sm"
|
||||
onClick={handleSaveClick}
|
||||
{isModelMenuOpen && portalRoot && modelMenuPosition && createPortal(
|
||||
<div
|
||||
ref={modelMenuPortalRef}
|
||||
className="model-nested-menu model-nested-menu--portal"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
disabled={!description.trim() || isSubmitting}
|
||||
data-testid="save-button"
|
||||
title="Create task"
|
||||
data-testid="model-nested-menu"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: `${modelMenuPosition.top}px`,
|
||||
left: `${modelMenuPosition.left}px`,
|
||||
width: `${modelMenuPosition.width}px`,
|
||||
}}
|
||||
>
|
||||
<Save size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Save
|
||||
</button>
|
||||
{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>,
|
||||
portalRoot,
|
||||
)}
|
||||
</div>
|
||||
<div className="quick-entry-hint">
|
||||
|
||||
@@ -1452,37 +1452,37 @@ describe("ListView Quick Entry", () => {
|
||||
expect(tableContainer?.contains(quickEntry)).toBe(true);
|
||||
});
|
||||
|
||||
it("shows model selector button when QuickEntryBox is expanded and actions dropdown is open", async () => {
|
||||
it("shows model selector action when QuickEntryBox is expanded and actions dropdown is open", async () => {
|
||||
const mockOnQuickCreate = vi.fn().mockResolvedValue(undefined);
|
||||
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||
|
||||
// Controls should start hidden (collapsed by default)
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
||||
|
||||
// Click the disclosure toggle to expand the QuickEntryBox controls
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
// Model selector button should be visible (controls are shown directly after toggle)
|
||||
const modelButton = await screen.findByTestId("quick-entry-models-button");
|
||||
expect(modelButton).toBeDefined();
|
||||
const actionsTrigger = await screen.findByTestId("quick-entry-actions-trigger");
|
||||
fireEvent.click(actionsTrigger);
|
||||
|
||||
const modelAction = await screen.findByTestId("quick-entry-actions-models");
|
||||
expect(modelAction).toBeDefined();
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
});
|
||||
|
||||
it("shows dependency selector button when QuickEntryBox is expanded and actions dropdown is open", async () => {
|
||||
it("shows dependency selector action when QuickEntryBox is expanded and actions dropdown is open", async () => {
|
||||
const mockOnQuickCreate = vi.fn().mockResolvedValue(undefined);
|
||||
renderListView({ onQuickCreate: mockOnQuickCreate });
|
||||
|
||||
// Controls should start hidden (collapsed by default)
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
||||
|
||||
// Click the disclosure toggle to expand the QuickEntryBox controls
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
// Dependency selector button should be visible (controls are shown directly after toggle)
|
||||
const depsButton = await screen.findByTestId("quick-entry-deps-button");
|
||||
expect(depsButton).toBeDefined();
|
||||
const actionsTrigger = await screen.findByTestId("quick-entry-actions-trigger");
|
||||
fireEvent.click(actionsTrigger);
|
||||
|
||||
const depsAction = await screen.findByTestId("quick-entry-actions-deps");
|
||||
expect(depsAction).toBeDefined();
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -2072,15 +2072,16 @@ describe("ListView - Bulk Selection", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
// Expand the QuickEntryBox and open the model menu
|
||||
// Expand the QuickEntryBox and open the model menu through actions
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
// Controls are shown directly after toggle (no actions dropdown)
|
||||
const modelButton = await screen.findByTestId("quick-entry-models-button");
|
||||
fireEvent.click(modelButton);
|
||||
const actionsTrigger = await screen.findByTestId("quick-entry-actions-trigger");
|
||||
fireEvent.click(actionsTrigger);
|
||||
|
||||
const modelsAction = await screen.findByTestId("quick-entry-actions-models");
|
||||
fireEvent.click(modelsAction);
|
||||
|
||||
// The nested model menu should render with data-testid
|
||||
const menu = await screen.findByTestId("model-nested-menu");
|
||||
expect(menu).toBeDefined();
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ vi.mock("lucide-react", () => ({
|
||||
ListTree: () => null,
|
||||
Sparkles: () => null,
|
||||
Save: () => null,
|
||||
MoreHorizontal: () => null,
|
||||
X: () => null,
|
||||
ChevronDown: () => null,
|
||||
ChevronUp: () => null,
|
||||
@@ -160,6 +161,25 @@ function expandQuickEntry() {
|
||||
fireEvent.click(toggleButton);
|
||||
}
|
||||
|
||||
function openActionsMenu() {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-actions-trigger"));
|
||||
}
|
||||
|
||||
function openDepsFromActions() {
|
||||
openActionsMenu();
|
||||
fireEvent.click(screen.getByTestId("quick-entry-actions-deps"));
|
||||
}
|
||||
|
||||
function openModelsFromActions() {
|
||||
openActionsMenu();
|
||||
fireEvent.click(screen.getByTestId("quick-entry-actions-models"));
|
||||
}
|
||||
|
||||
function clickSaveFromActions() {
|
||||
openActionsMenu();
|
||||
fireEvent.click(screen.getByTestId("quick-entry-actions-save"));
|
||||
}
|
||||
|
||||
describe("QuickEntryBox", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
@@ -500,7 +520,7 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
|
||||
describe("Rich creation features", () => {
|
||||
it("shows dependency button when expanded", () => {
|
||||
it("shows actions trigger when expanded", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Initially, controls region is collapsed/hidden
|
||||
@@ -511,11 +531,11 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
||||
|
||||
// The dependency button should be directly visible in the disclosure panel
|
||||
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
||||
// Advanced controls are behind the actions trigger
|
||||
expect(screen.getByTestId("quick-entry-actions-trigger")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows model selector button when expanded", () => {
|
||||
it("shows actions menu items when actions trigger is clicked", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Initially, controls region is collapsed/hidden
|
||||
@@ -526,8 +546,11 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
|
||||
// The model selector button should be directly visible in the disclosure panel
|
||||
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
|
||||
openActionsMenu();
|
||||
|
||||
expect(screen.getByTestId("quick-entry-actions-deps")).toBeTruthy();
|
||||
expect(screen.getByTestId("quick-entry-actions-models")).toBeTruthy();
|
||||
expect(screen.getByTestId("quick-entry-actions-save")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows Plan and Subtask buttons when expanded", () => {
|
||||
@@ -552,7 +575,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-deps-button"));
|
||||
openDepsFromActions();
|
||||
|
||||
// Dropdown should be visible with search input
|
||||
expect(document.querySelector(".dep-dropdown")).toBeTruthy();
|
||||
@@ -570,7 +593,7 @@ describe("QuickEntryBox", () => {
|
||||
expect(screen.queryByTestId("model-nested-menu")).toBeNull();
|
||||
|
||||
// Click the models button
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
// Menu should now be visible
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
@@ -582,7 +605,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
expect(screen.getByTestId("model-menu-plan")).toBeTruthy();
|
||||
expect(screen.getByTestId("model-menu-executor")).toBeTruthy();
|
||||
@@ -595,7 +618,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||
|
||||
// Submenu should show the dropdown for executor
|
||||
@@ -610,7 +633,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-plan"));
|
||||
|
||||
expect(screen.getByTestId("custom-model-dropdown-plan model")).toBeTruthy();
|
||||
@@ -622,7 +645,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-validator"));
|
||||
|
||||
expect(screen.getByTestId("custom-model-dropdown-validator model")).toBeTruthy();
|
||||
@@ -634,7 +657,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||
|
||||
// Click back
|
||||
@@ -652,7 +675,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||
|
||||
// Should be in submenu — back button visible
|
||||
@@ -675,7 +698,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-plan"));
|
||||
|
||||
// Select a model via mocked dropdown
|
||||
@@ -698,7 +721,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-validator"));
|
||||
|
||||
// Select a model via mocked dropdown
|
||||
@@ -719,7 +742,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-plan"));
|
||||
|
||||
// Select then clear model
|
||||
@@ -741,7 +764,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-deps-button"));
|
||||
openDepsFromActions();
|
||||
|
||||
// Click on a task to select it
|
||||
const taskItem = document.querySelector(".dep-dropdown-item");
|
||||
@@ -883,7 +906,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with model" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
// Menu should be open
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
@@ -917,7 +940,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with menu" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
// Menu should be open
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
@@ -932,31 +955,44 @@ describe("QuickEntryBox", () => {
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("Task with menu");
|
||||
});
|
||||
|
||||
it("Escape hierarchy: model submenu → model menu → deps popover → input clear", () => {
|
||||
it("Escape hierarchy: actions menu → model submenu → model menu → deps popover → input clear", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
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();
|
||||
openActionsMenu();
|
||||
expect(screen.getByTestId("quick-entry-actions-deps")).toBeTruthy();
|
||||
|
||||
// Open executor submenu
|
||||
// Escape 1: close actions menu first
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
expect(screen.queryByTestId("quick-entry-actions-deps")).toBeNull();
|
||||
|
||||
// Re-open via actions and go to model submenu
|
||||
openModelsFromActions();
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||
expect(screen.getByTestId("model-submenu-back")).toBeTruthy();
|
||||
|
||||
// Escape 1: close submenu → back to model menu top level
|
||||
// Escape 2: 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
|
||||
// Escape 3: close model menu
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
expect(screen.queryByTestId("model-nested-menu")).toBeNull();
|
||||
|
||||
// Escape 3: clear input and collapse
|
||||
// Open deps popover via actions
|
||||
openDepsFromActions();
|
||||
expect(document.querySelector(".dep-dropdown")).toBeTruthy();
|
||||
|
||||
// Escape 4: close deps popover
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
expect(document.querySelector(".dep-dropdown")).toBeNull();
|
||||
|
||||
// Escape 5: 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);
|
||||
@@ -985,6 +1021,123 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Actions dropdown behavior (FN-1088)", () => {
|
||||
it("clicking Deps in actions closes actions menu and opens dep dropdown", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task with deps" } });
|
||||
|
||||
openDepsFromActions();
|
||||
|
||||
expect(screen.queryByTestId("quick-entry-actions-deps")).toBeNull();
|
||||
expect(document.querySelector(".dep-dropdown")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking Models in actions closes actions menu and opens model menu", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
|
||||
openModelsFromActions();
|
||||
|
||||
expect(screen.queryByTestId("quick-entry-actions-models")).toBeNull();
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Escape closes actions menu first", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Escape test" } });
|
||||
openActionsMenu();
|
||||
expect(screen.getByTestId("quick-entry-actions-save")).toBeTruthy();
|
||||
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
|
||||
expect(screen.queryByTestId("quick-entry-actions-save")).toBeNull();
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("Escape test");
|
||||
});
|
||||
|
||||
it("clicking outside actions menu closes it without side effects", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Outside click test" } });
|
||||
openActionsMenu();
|
||||
expect(screen.getByTestId("quick-entry-actions-deps")).toBeTruthy();
|
||||
|
||||
const outsideElement = document.createElement("div");
|
||||
document.body.appendChild(outsideElement);
|
||||
try {
|
||||
fireEvent.mouseDown(outsideElement);
|
||||
} finally {
|
||||
document.body.removeChild(outsideElement);
|
||||
}
|
||||
|
||||
expect(screen.queryByTestId("quick-entry-actions-deps")).toBeNull();
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("Outside click test");
|
||||
expect(document.querySelector(".dep-dropdown")).toBeNull();
|
||||
expect(screen.queryByTestId("model-nested-menu")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows badge count when deps and model overrides are selected", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Badge test" } });
|
||||
|
||||
openDepsFromActions();
|
||||
const firstDep = document.querySelector(".dep-dropdown-item");
|
||||
expect(firstDep).toBeTruthy();
|
||||
fireEvent.click(firstDep!);
|
||||
|
||||
openModelsFromActions();
|
||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||
fireEvent.click(screen.getByTestId("dropdown-select-executor model"));
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
|
||||
expect(screen.getByTestId("quick-entry-actions-badge").textContent).toBe("2");
|
||||
});
|
||||
|
||||
it("hides badge when no deps or model overrides are selected", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
expect(screen.queryByTestId("quick-entry-actions-badge")).toBeNull();
|
||||
});
|
||||
|
||||
it("dependency selections persist when actions menu closes via outside click", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Dependency persistence" } });
|
||||
|
||||
openDepsFromActions();
|
||||
const firstDep = document.querySelector(".dep-dropdown-item");
|
||||
expect(firstDep).toBeTruthy();
|
||||
fireEvent.click(firstDep!);
|
||||
expect(document.querySelector(".dep-dropdown-item.selected")).toBeTruthy();
|
||||
|
||||
openActionsMenu();
|
||||
const outsideElement = document.createElement("div");
|
||||
document.body.appendChild(outsideElement);
|
||||
try {
|
||||
fireEvent.mouseDown(outsideElement);
|
||||
} finally {
|
||||
document.body.removeChild(outsideElement);
|
||||
}
|
||||
|
||||
openDepsFromActions();
|
||||
expect(document.querySelector(".dep-dropdown-item.selected")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("State sync between isExpanded and isDisclosureExpanded", () => {
|
||||
it("focus then toggle shows controls without collapsing textarea", () => {
|
||||
renderQuickEntryBox();
|
||||
@@ -1227,7 +1380,7 @@ describe("QuickEntryBox", () => {
|
||||
|
||||
// Type something and open dropdown
|
||||
fireEvent.change(textarea, { target: { value: "Task with dropdown" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-deps-button"));
|
||||
openDepsFromActions();
|
||||
|
||||
// localStorage should have the value
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Task with dropdown");
|
||||
@@ -1476,64 +1629,27 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Save button", () => {
|
||||
it("shows save button when expanded", () => {
|
||||
describe("Save action", () => {
|
||||
it("shows save action in the actions dropdown", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Initially, controls region is collapsed/hidden
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
||||
|
||||
// Expand and type something
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
|
||||
// The save button should be directly visible in the disclosure panel
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
openActionsMenu();
|
||||
expect(screen.getByTestId("quick-entry-actions-save")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("save button is disabled when textarea is empty", () => {
|
||||
it("save action is disabled when textarea is empty", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Type something
|
||||
fireEvent.change(textarea, { target: { value: "Some text" } });
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
|
||||
// Clear the input
|
||||
fireEvent.change(textarea, { target: { value: "" } });
|
||||
|
||||
// Button should be hidden or disabled when input is empty
|
||||
const saveButton = screen.queryByTestId("save-button") as HTMLButtonElement | null;
|
||||
if (saveButton) {
|
||||
expect(saveButton.disabled).toBe(true);
|
||||
}
|
||||
openActionsMenu();
|
||||
const saveButton = screen.getByTestId("quick-entry-actions-save") as HTMLButtonElement;
|
||||
expect(saveButton.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("save button is disabled during submission", async () => {
|
||||
const { props } = renderQuickEntryBox({});
|
||||
// Slow down the promise to see loading state
|
||||
props.onCreate.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
|
||||
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "New task" } });
|
||||
|
||||
// Start submission with Enter key
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
// During submission, button should be disabled
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.queryByTestId("save-button") as HTMLButtonElement | null;
|
||||
if (saveButton) {
|
||||
expect(saveButton.disabled).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("clicking save button persists to localStorage", async () => {
|
||||
it("clicking save action persists to localStorage", async () => {
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
@@ -1544,28 +1660,23 @@ describe("QuickEntryBox", () => {
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBe("Draft task description");
|
||||
});
|
||||
|
||||
// Click the save button
|
||||
fireEvent.click(screen.getByTestId("save-button"));
|
||||
clickSaveFromActions();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// localStorage should be cleared after successful submission
|
||||
expect(localStorage.getItem("kb-quick-entry-text")).toBeNull();
|
||||
});
|
||||
|
||||
it("clicking save button creates the task", async () => {
|
||||
it("clicking save action creates the task", async () => {
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
clickSaveFromActions();
|
||||
|
||||
// Click the save button
|
||||
fireEvent.click(screen.getByTestId("save-button"));
|
||||
|
||||
// Task should be created
|
||||
await waitFor(() => {
|
||||
expect(props.onCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -1576,127 +1687,81 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("save button has correct test id", () => {
|
||||
it("save action has correct metadata", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
openActionsMenu();
|
||||
|
||||
// Button should have data-testid="save-button"
|
||||
const saveButton = screen.getByTestId("save-button");
|
||||
expect(saveButton).toBeTruthy();
|
||||
// Save button uses theme-driven class for task creation CTA
|
||||
const saveButton = screen.getByTestId("quick-entry-actions-save");
|
||||
expect(saveButton.className).toContain("btn-task-create");
|
||||
});
|
||||
|
||||
it("save button has correct title attribute", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
|
||||
const saveButton = screen.getByTestId("save-button");
|
||||
expect(saveButton.getAttribute("title")).toBe("Create task");
|
||||
});
|
||||
|
||||
it("save button prevents textarea blur on mousedown", () => {
|
||||
it("save action prevents textarea blur on mousedown", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task to save" } });
|
||||
openActionsMenu();
|
||||
|
||||
// Get save button and trigger mousedown (prevents blur)
|
||||
const saveButton = screen.getByTestId("save-button");
|
||||
const saveButton = screen.getByTestId("quick-entry-actions-save");
|
||||
fireEvent.mouseDown(saveButton);
|
||||
|
||||
// Trigger blur on textarea - the 200ms timer would collapse controls
|
||||
// but since we called mousedown with preventDefault first, it should be blocked
|
||||
// The blur event should still fire, but the onMouseDown handler prevents focus loss
|
||||
fireEvent.blur(textarea);
|
||||
|
||||
// Controls should still be visible immediately after blur (blur timeout hasn't fired yet)
|
||||
// The real protection happens during the mousedown event before blur
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
|
||||
expect(screen.getByTestId("quick-entry-actions-save")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Button visibility when collapsed", () => {
|
||||
it("controls div has hidden attribute when not expanded", () => {
|
||||
renderQuickEntryBox({});
|
||||
// Component starts collapsed (disclosure expanded state is false)
|
||||
// Only the toggle button should be visible, all other buttons should be hidden
|
||||
|
||||
const controls = document.getElementById("quick-entry-controls");
|
||||
expect(controls?.hasAttribute("hidden")).toBe(true);
|
||||
});
|
||||
|
||||
it("toggle button is always visible regardless of expanded state", () => {
|
||||
renderQuickEntryBox({});
|
||||
// Toggle button should always be visible
|
||||
expect(screen.getByTestId("quick-entry-toggle")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows buttons after clicking toggle to expand", () => {
|
||||
it("shows actions trigger and direct Plan/Subtask/Refine controls after expand", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Initially collapsed - controls hidden
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
||||
|
||||
// Click toggle to expand
|
||||
expandQuickEntry();
|
||||
|
||||
// Now controls should be visible
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
// 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 in the actions area inside the disclosure panel
|
||||
expect(screen.getByTestId("quick-entry-actions-trigger")).toBeTruthy();
|
||||
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("subtask-button")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides buttons again after collapsing via toggle", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Expand
|
||||
expandQuickEntry();
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
|
||||
|
||||
// Collapse
|
||||
expandQuickEntry();
|
||||
|
||||
// Controls should be hidden again
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
||||
});
|
||||
|
||||
it("verifies all buttons are accessible when expanded", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
// 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-models-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("plan-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("subtask-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("save-button")).toBeTruthy();
|
||||
expect(screen.getByTestId("refine-button")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides controls again after collapsing via toggle", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
expandQuickEntry();
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
|
||||
expect(screen.getByTestId("quick-entry-actions-trigger")).toBeTruthy();
|
||||
|
||||
expandQuickEntry();
|
||||
|
||||
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Consolidated actions layout (FN-781, FN-973)", () => {
|
||||
describe("Consolidated actions layout (FN-781, FN-1088)", () => {
|
||||
it("renders Plan, Subtask, and Refine in actions area inside controls panel", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
// 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-actions");
|
||||
expect(actionsContainer.contains(screen.getByTestId("plan-button"))).toBe(true);
|
||||
expect(actionsContainer.contains(screen.getByTestId("subtask-button"))).toBe(true);
|
||||
@@ -1705,37 +1770,24 @@ describe("QuickEntryBox", () => {
|
||||
|
||||
it("does not render actions when not expanded", () => {
|
||||
renderQuickEntryBox({});
|
||||
|
||||
// Actions should not exist when collapsed
|
||||
expect(screen.queryByTestId("quick-entry-actions")).toBeNull();
|
||||
});
|
||||
|
||||
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-actions");
|
||||
const saveButton = screen.getByTestId("save-button");
|
||||
|
||||
// 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 actions area", () => {
|
||||
it("renders advanced controls only inside the actions dropdown", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
fireEvent.change(textarea, { target: { value: "Task text" } });
|
||||
|
||||
const actionsContainer = screen.getByTestId("quick-entry-actions");
|
||||
const depsButton = screen.getByTestId("quick-entry-deps-button");
|
||||
const modelsButton = screen.getByTestId("quick-entry-models-button");
|
||||
expect(screen.queryByTestId("quick-entry-actions-deps")).toBeNull();
|
||||
expect(screen.queryByTestId("quick-entry-actions-models")).toBeNull();
|
||||
expect(screen.queryByTestId("quick-entry-actions-save")).toBeNull();
|
||||
|
||||
// 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);
|
||||
openActionsMenu();
|
||||
|
||||
expect(screen.getByTestId("quick-entry-actions-deps")).toBeTruthy();
|
||||
expect(screen.getByTestId("quick-entry-actions-models")).toBeTruthy();
|
||||
expect(screen.getByTestId("quick-entry-actions-save")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Plan button disabled state still works in actions area", () => {
|
||||
@@ -1743,41 +1795,30 @@ describe("QuickEntryBox", () => {
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Plan button should be disabled when description is empty
|
||||
expect((screen.getByTestId("plan-button") as HTMLButtonElement).disabled).toBe(true);
|
||||
|
||||
// Type something — Plan should become enabled
|
||||
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", () => {
|
||||
it("shows Plan/Subtask/Refine together with the actions trigger when disclosure is expanded", () => {
|
||||
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();
|
||||
expect(screen.getByTestId("quick-entry-actions-trigger")).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);
|
||||
expect(controlsPanel?.contains(screen.getByTestId("quick-entry-actions-trigger"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1802,7 +1843,7 @@ describe("QuickEntryBox", () => {
|
||||
expandQuickEntry();
|
||||
|
||||
// Open model menu
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
@@ -1834,7 +1875,7 @@ describe("QuickEntryBox", () => {
|
||||
expandQuickEntry();
|
||||
|
||||
// Open model menu and select an executor via submenu
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
@@ -1887,7 +1928,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
expect(menu).toBeTruthy();
|
||||
@@ -1905,7 +1946,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
expect(menu).toBeTruthy();
|
||||
@@ -1922,7 +1963,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
|
||||
// Navigate to executor submenu (shows CustomModelDropdown mock)
|
||||
@@ -1955,7 +1996,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
expect(menu).toBeTruthy();
|
||||
|
||||
@@ -1972,7 +2013,7 @@ describe("QuickEntryBox", () => {
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with menu" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
|
||||
|
||||
// Click on an element outside both the trigger and the portal
|
||||
@@ -1992,7 +2033,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
const initialTop = menu.style.top;
|
||||
|
||||
@@ -2013,7 +2054,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
|
||||
// On mobile, width should be viewport width minus padding (375 - 32 = 343)
|
||||
@@ -2027,7 +2068,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
|
||||
// Left should be clamped to at least 16px (horizontal padding)
|
||||
@@ -2042,7 +2083,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
|
||||
const menuLeft = parseFloat(menu.style.left);
|
||||
@@ -2059,7 +2100,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
|
||||
const menuWidth = parseFloat(menu.style.width);
|
||||
@@ -2074,7 +2115,7 @@ describe("QuickEntryBox", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
openModelsFromActions();
|
||||
const menu = screen.getByTestId("model-nested-menu");
|
||||
|
||||
// Desktop width
|
||||
|
||||
@@ -13401,7 +13401,26 @@ html .column.drag-over * {
|
||||
padding: 3px 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-entry-actions-badge {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
padding: 0 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--todo);
|
||||
color: var(--bg);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions-dropdown {
|
||||
|
||||
Reference in New Issue
Block a user