FN-6131: move quick entry save and toggles to top

Reorder the Quick Entry action row so the primary create controls appear first.

- move Save, Fast, GitHub, and Priority controls to the start of the Quick Entry actions row
- preserve the existing priority picker behavior while relocating its trigger and portal markup
- update QuickEntryBox tests to assert the new DOM order and keep focus-coverage stable after the reorder

Files changed:
 packages/dashboard/app/components/QuickEntryBox.tsx     | 225 +++++++++++----------
 packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx    |  47 +++--
 2 files changed, 145 insertions(+), 127 deletions(-)

Fusion-Task-Id: FN-6131

Fusion-Task-Lineage: d95401b9-4440-4c55-ad32-94df7d0c33fe
This commit is contained in:
gsxdsm
2026-06-09 15:02:53 -07:00
parent 7f08845ed5
commit 0b589e1f2b
2 changed files with 145 additions and 127 deletions

View File

@@ -1507,6 +1507,119 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
touchButtonRef.current = null;
}}
>
<button
type="button"
className="btn btn-task-create btn-sm"
onClick={handleSaveClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim() || isSubmitting}
data-testid="quick-entry-save"
title={t("tasks.createTaskTitle", "Create task")}
>
<Save size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("tasks.save", "Save")}
</button>
<button
type="button"
className={`btn btn-sm ${isFastMode ? "btn-primary" : ""}`}
onClick={() => setIsFastMode((prev) => !prev)}
onMouseDown={(e) => e.preventDefault()}
aria-pressed={isFastMode}
data-testid="quick-entry-fast-toggle"
title={t("tasks.toggleFastMode", "Toggle fast execution mode")}
>
{t("tasks.fast", "Fast")}
</button>
<button
type="button"
className={`btn btn-sm ${githubTrackingProjectEnabled && effectiveGithubTracking ? "btn-primary" : ""}`}
onClick={() => {
if (!githubTrackingProjectEnabled) {
return;
}
setGithubTrackingOverride((prev) => (prev ?? true) ? false : true);
}}
onMouseDown={(e) => e.preventDefault()}
disabled={!githubTrackingProjectEnabled}
aria-pressed={effectiveGithubTracking}
aria-disabled={!githubTrackingProjectEnabled || undefined}
data-testid="quick-entry-github-toggle"
title={githubToggleLabel}
aria-label={githubToggleLabel}
>
<ProviderIcon provider="github" size="sm" />
</button>
<div className="priority-trigger-wrap" ref={priorityPickerRef}>
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm dep-trigger"
data-testid="quick-entry-priority-button"
onClick={() => {
setShowDeps(false);
setShowAgentPicker(false);
setAgentPickerPosition(null);
setShowNodePicker(false);
setNodePickerPosition(null);
setIsModelMenuOpen(false);
setModelMenuPosition(null);
setActiveModelSubmenu(null);
setShowPriorityPicker((prev) => {
const next = !prev;
if (next) {
updatePriorityPickerPosition();
} else {
setPriorityPickerPosition(null);
}
return next;
});
}}
>
<Flag size={12} style={{ verticalAlign: "middle" }} />
{` ${priority[0].toUpperCase()}${priority.slice(1)}`}
</button>
</div>
{showPriorityPicker && portalRoot && priorityPickerPosition && createPortal(
<div
ref={priorityPickerPortalRef}
className="dep-dropdown priority-picker-dropdown priority-picker-dropdown--portal"
onMouseDown={(e) => e.preventDefault()}
style={{
position: "fixed",
top: `${priorityPickerPosition.top}px`,
left: `${priorityPickerPosition.left}px`,
width: `${priorityPickerPosition.width}px`,
maxHeight: priorityPickerPosition.maxHeight ? `${priorityPickerPosition.maxHeight}px` : undefined,
overflowY: priorityPickerPosition.maxHeight ? "auto" : undefined,
}}
>
<div className="dep-dropdown-search-header">{t("tasks.selectPriority", "Select priority")}</div>
{TASK_PRIORITIES.map((taskPriority) => {
const label = `${taskPriority[0].toUpperCase()}${taskPriority.slice(1)}`;
return (
<div
key={taskPriority}
className={`dep-dropdown-item${priority === taskPriority ? " selected" : ""}`}
data-testid={`quick-entry-priority-option-${taskPriority}`}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setPriority(taskPriority);
setShowPriorityPicker(false);
setPriorityPickerPosition(null);
}}
>
<span className="dep-dropdown-title">{label}</span>
</div>
);
})}
</div>,
portalRoot,
)}
<button
type="button"
className="btn btn-sm"
@@ -1893,118 +2006,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
portalRoot,
)}
<div className="priority-trigger-wrap" ref={priorityPickerRef}>
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm dep-trigger"
data-testid="quick-entry-priority-button"
onClick={() => {
setShowDeps(false);
setShowAgentPicker(false);
setAgentPickerPosition(null);
setShowNodePicker(false);
setNodePickerPosition(null);
setIsModelMenuOpen(false);
setModelMenuPosition(null);
setActiveModelSubmenu(null);
setShowPriorityPicker((prev) => {
const next = !prev;
if (next) {
updatePriorityPickerPosition();
} else {
setPriorityPickerPosition(null);
}
return next;
});
}}
>
<Flag size={12} style={{ verticalAlign: "middle" }} />
{` ${priority[0].toUpperCase()}${priority.slice(1)}`}
</button>
</div>
{showPriorityPicker && portalRoot && priorityPickerPosition && createPortal(
<div
ref={priorityPickerPortalRef}
className="dep-dropdown priority-picker-dropdown priority-picker-dropdown--portal"
onMouseDown={(e) => e.preventDefault()}
style={{
position: "fixed",
top: `${priorityPickerPosition.top}px`,
left: `${priorityPickerPosition.left}px`,
width: `${priorityPickerPosition.width}px`,
maxHeight: priorityPickerPosition.maxHeight ? `${priorityPickerPosition.maxHeight}px` : undefined,
overflowY: priorityPickerPosition.maxHeight ? "auto" : undefined,
}}
>
<div className="dep-dropdown-search-header">{t("tasks.selectPriority", "Select priority")}</div>
{TASK_PRIORITIES.map((taskPriority) => {
const label = `${taskPriority[0].toUpperCase()}${taskPriority.slice(1)}`;
return (
<div
key={taskPriority}
className={`dep-dropdown-item${priority === taskPriority ? " selected" : ""}`}
data-testid={`quick-entry-priority-option-${taskPriority}`}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setPriority(taskPriority);
setShowPriorityPicker(false);
setPriorityPickerPosition(null);
}}
>
<span className="dep-dropdown-title">{label}</span>
</div>
);
})}
</div>,
portalRoot,
)}
<button
type="button"
className={`btn btn-sm ${isFastMode ? "btn-primary" : ""}`}
onClick={() => setIsFastMode((prev) => !prev)}
onMouseDown={(e) => e.preventDefault()}
aria-pressed={isFastMode}
data-testid="quick-entry-fast-toggle"
title={t("tasks.toggleFastMode", "Toggle fast execution mode")}
>
{t("tasks.fast", "Fast")}
</button>
<button
type="button"
className={`btn btn-sm ${githubTrackingProjectEnabled && effectiveGithubTracking ? "btn-primary" : ""}`}
onClick={() => {
if (!githubTrackingProjectEnabled) {
return;
}
setGithubTrackingOverride((prev) => (prev ?? true) ? false : true);
}}
onMouseDown={(e) => e.preventDefault()}
disabled={!githubTrackingProjectEnabled}
aria-pressed={effectiveGithubTracking}
aria-disabled={!githubTrackingProjectEnabled || undefined}
data-testid="quick-entry-github-toggle"
title={githubToggleLabel}
aria-label={githubToggleLabel}
>
<ProviderIcon provider="github" size="sm" />
</button>
<button
type="button"
className="btn btn-task-create btn-sm"
onClick={handleSaveClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim() || isSubmitting}
data-testid="quick-entry-save"
title={t("tasks.createTaskTitle", "Create task")}
>
<Save size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("tasks.save", "Save")}
</button>
</div>
)}

View File

@@ -262,6 +262,10 @@ function mockMobileViewport() {
}
const QUICK_ENTRY_ACTION_BUTTONS = [
["Save", "quick-entry-save"],
["Fast", "quick-entry-fast-toggle"],
["GitHub", "quick-entry-github-toggle"],
["Priority", "quick-entry-priority-button"],
["Plan", "plan-button"],
["Subtask", "subtask-button"],
["Refine", "refine-button"],
@@ -270,10 +274,6 @@ const QUICK_ENTRY_ACTION_BUTTONS = [
["Models", "quick-entry-models"],
["Node", "quick-entry-node-button"],
["Agent", "quick-entry-agent-button"],
["Priority", "quick-entry-priority-button"],
["Fast", "quick-entry-fast-toggle"],
["GitHub", "quick-entry-github-toggle"],
["Save", "quick-entry-save"],
] as const;
describe("QuickEntryBox", () => {
@@ -366,15 +366,14 @@ describe("QuickEntryBox", () => {
["Priority", "quick-entry-priority-button"],
] as const;
const allActionButtons = [
["Plan", "plan-button"],
["Subtask", "subtask-button"],
["Refine", "refine-button"],
...newlyCoveredActionButtons,
["Fast", "quick-entry-fast-toggle"],
["GitHub", "quick-entry-github-toggle"],
["Save", "quick-entry-save"],
] as const;
const allActionButtons = QUICK_ENTRY_ACTION_BUTTONS;
const actionButtonsWithSaveLast = [...allActionButtons.slice(1), allActionButtons[0]];
function getActionButtonTestIdsInDomOrder() {
const actionsContainer = screen.getByTestId("quick-entry-actions");
return Array.from(actionsContainer.querySelectorAll<HTMLButtonElement>("button[data-testid]"))
.map((button) => button.dataset.testid);
}
function focusTextareaWithValue(value: string) {
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
@@ -414,6 +413,20 @@ describe("QuickEntryBox", () => {
});
});
it("Save button is the first action button in DOM order", () => {
renderQuickEntryBox({});
expandQuickEntry();
expect(getActionButtonTestIdsInDomOrder()[0]).toBe("quick-entry-save");
});
it("action buttons appear in correct DOM order after reorder", () => {
renderQuickEntryBox({});
expandQuickEntry();
expect(getActionButtonTestIdsInDomOrder()).toEqual(allActionButtons.map(([_label, testId]) => testId));
});
it("keeps textarea focused for every quick-entry action button", async () => {
mockDesktopViewport();
vi.mocked(fetchSettings).mockResolvedValueOnce({
@@ -427,7 +440,7 @@ describe("QuickEntryBox", () => {
});
const actionsContainer = screen.getByTestId("quick-entry-actions");
for (const [_label, testId] of allActionButtons) {
for (const [_label, testId] of actionButtonsWithSaveLast) {
const textarea = focusTextareaWithValue(`Focus preserved for ${testId}`);
const button = screen.getByTestId(testId);
expect(actionsContainer.contains(button)).toBe(true);
@@ -524,8 +537,12 @@ describe("QuickEntryBox", () => {
const actionsContainer = screen.getByTestId("quick-entry-actions");
const buttons = Array.from(actionsContainer.querySelectorAll("button"));
expect(buttons).toHaveLength(QUICK_ENTRY_ACTION_BUTTONS.length);
const buttonsWithSaveLast = [
...buttons.filter((button) => button.dataset.testid !== "quick-entry-save"),
...buttons.filter((button) => button.dataset.testid === "quick-entry-save"),
];
for (const button of buttons) {
for (const button of buttonsWithSaveLast) {
const textarea = focusTextareaWithValue(`Full mobile surface focus for ${button.dataset.testid ?? button.textContent}`);
await touchActionButton(button);
expect(document.activeElement).toBe(textarea);