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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user