FN-7805: redesign quick-add priority indicators as icon-only controls
Redesigns the Quick Add composer's priority selector to icon-only glyph buttons and relocates GitHub tracking beside attach, backed by a new shared priorityIndicator helper. - Add packages/dashboard/app/utils/priorityIndicator.tsx as the single source of ArrowUp/ArrowDown/Flag/TriangleAlert priority glyphs, shared by QuickEntryBox and TaskForm - Rework QuickEntryBox.tsx to render icon-only priority/Fast controls and move GitHub tracking + Priority into the .quick-entry-primary-group, with matching QuickEntryBox.css layout updates - Update TaskForm.tsx to reuse the new priorityIndicator helper instead of duplicating icon/label logic - Update quick-entry height-parity/trigger-height tests and QuickEntryBox/TaskForm/NewTaskModal tests for the new icon-only layout - Add priorityIndicator.test.tsx covering the shared helper - Update docs/dashboard-guide.md and add changeset fn-7805-quick-add-priority-indicators.md (patch, feature) Files changed: .../fn-7805-quick-add-priority-indicators.md | 7 + docs/dashboard-guide.md | 3 +- .../quick-entry-action-row-height-parity.test.tsx | 13 +- .../quick-entry-workflow-trigger-height.test.tsx | 10 +- .../dashboard/app/components/QuickEntryBox.css | 19 +- .../dashboard/app/components/QuickEntryBox.tsx | 197 +++++++++++---------- packages/dashboard/app/components/TaskForm.tsx | 33 ++-- .../app/components/__tests__/NewTaskModal.test.tsx | 3 + .../components/__tests__/QuickEntryBox.test.tsx | 113 ++++++++---- .../app/components/__tests__/TaskForm.test.tsx | 3 + .../app/utils/__tests__/priorityIndicator.test.tsx | 21 +++ packages/dashboard/app/utils/priorityIndicator.tsx | 30 ++++ 12 files changed, 300 insertions(+), 152 deletions(-) Fusion-Task-Id: FN-7805 Fusion-Task-Lineage: 404fe8d6-0851-44cb-b31d-f47896aaaaba Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7805-quick-add-priority-indicators.md
Normal file
7
.changeset/fn-7805-quick-add-priority-indicators.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Quick-add composer now shows icon-only priority/Fast controls with GitHub tracking beside attach.
|
||||
category: feature
|
||||
dev: QuickEntryBox + TaskForm reuse a shared priorityIndicator glyph helper; GitHub + Priority relocated into .quick-entry-primary-group; no test-id/payload changes.
|
||||
@@ -462,7 +462,8 @@ Create requests never send an explicit `column`. The task store resolves the lan
|
||||
Optional workflow steps declared by the active workflow are available from the quick-add action row and the **New Task** dialog's inline quick buttons. For example, the coding workflow's browser verification option appears as a quick drop-down when that workflow is active; each option is seeded from the workflow step's `defaultOn` setting and is sent with the task's `enabledWorkflowSteps` payload at creation time.
|
||||
|
||||
<!-- FNXC:QuickAddAttachments 2026-06-30-00:00: Quick Add attachments use a compact icon-only paperclip while keeping the Attach action and pending image count in accessible labels. The same pending preview/upload path accepts image selection, paste, and direct drag/drop onto the Quick Add box. -->
|
||||
Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show pending previews before task creation and upload the images to the created task afterward.
|
||||
<!-- FNXC:QuickAddPriorityIndicator 2026-07-10-21:45: Quick Add keeps status controls in the bottom action cluster: GitHub tracking sits beside the paperclip attach button, Priority is icon-only with low/down, normal/flag, high/up, urgent/alert glyphs, and Fast is an icon-only lightning button. -->
|
||||
Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show pending previews before task creation and upload the images to the created task afterward. The same bottom action row places the GitHub tracking override beside the paperclip; Priority is an icon-only control whose glyph changes by selected level (down arrow for low, flag for normal, up arrow for high, alert for urgent), and Fast is an icon-only lightning control. These icon-only controls keep accessible labels and the same create-payload behavior as the previous text chips.
|
||||
|
||||
Quick entry, inline quick-create, and the full **New Task** dialog all check for similar active tasks before creating. When possible duplicates exist, the warning lists each match by task description (falling back to title, then “No description”) and lets you open an existing task, cancel, or create anyway with the duplicates acknowledged.
|
||||
|
||||
|
||||
@@ -89,7 +89,11 @@ vi.mock("lucide-react", () => ({
|
||||
ChevronRight: () => null,
|
||||
Bot: () => null,
|
||||
Server: () => null,
|
||||
ArrowDown: () => null,
|
||||
ArrowUp: () => null,
|
||||
Flag: () => null,
|
||||
TriangleAlert: () => null,
|
||||
Zap: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
}));
|
||||
@@ -182,7 +186,7 @@ describe("quick-entry action row height parity (FN-7680)", () => {
|
||||
// unrelated @media block or the desktop base rule further up the file.
|
||||
const sectionStart = cssContent.indexOf("Quick Entry Mobile Touch + Overflow Fixes");
|
||||
expect(sectionStart).toBeGreaterThan(-1);
|
||||
const section = cssContent.slice(sectionStart, sectionStart + 800);
|
||||
const section = cssContent.slice(sectionStart, sectionStart + 1600);
|
||||
|
||||
expect(section).toContain("max-width: 768px");
|
||||
const mobileBlockMatch = section.match(
|
||||
@@ -221,7 +225,7 @@ describe("quick-entry action row height parity (FN-7680)", () => {
|
||||
expect(depTriggerMatch![1].trim()).toBe("3px 8px");
|
||||
});
|
||||
|
||||
it("renders Save, workflow trigger, Attach, Fast, Priority, and Deps as sibling .btn elements in the same .quick-entry-actions row", () => {
|
||||
it("renders Save, workflow trigger, Attach, GitHub, Priority, Fast, and Deps as sibling .btn elements in the same .quick-entry-actions row", () => {
|
||||
mockDesktopViewport();
|
||||
renderQuickEntryBox();
|
||||
|
||||
@@ -231,11 +235,12 @@ describe("quick-entry action row height parity (FN-7680)", () => {
|
||||
const save = screen.getByTestId("quick-entry-save");
|
||||
const trigger = screen.getByTestId("quick-entry-workflow-trigger");
|
||||
const attach = screen.getByTestId("quick-entry-attach");
|
||||
const fast = screen.getByTestId("quick-entry-fast-toggle");
|
||||
const github = screen.getByTestId("quick-entry-github-toggle");
|
||||
const priority = screen.getByTestId("quick-entry-priority-button");
|
||||
const fast = screen.getByTestId("quick-entry-fast-toggle");
|
||||
const deps = screen.getByTestId("quick-entry-deps");
|
||||
|
||||
for (const el of [save, trigger, attach, fast, priority, deps]) {
|
||||
for (const el of [save, trigger, attach, github, priority, fast, deps]) {
|
||||
expect(el.classList.contains("btn")).toBe(true);
|
||||
expect(actionsRow.contains(el)).toBe(true);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,11 @@ vi.mock("lucide-react", () => ({
|
||||
ChevronRight: () => null,
|
||||
Bot: () => null,
|
||||
Server: () => null,
|
||||
ArrowDown: () => null,
|
||||
ArrowUp: () => null,
|
||||
Flag: () => null,
|
||||
TriangleAlert: () => null,
|
||||
Zap: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
}));
|
||||
@@ -366,18 +370,22 @@ describe("quick-entry-actions fixed-height parity, not just a min-height floor (
|
||||
expect(depTriggerMatches!.length).toBe(1);
|
||||
});
|
||||
|
||||
it("renders Save, Attach, Fast, and the workflow trigger as siblings in one .quick-entry-actions row (no shell/layout regression; jsdom cannot measure real pixel heights)", () => {
|
||||
it("renders Save, Attach, GitHub, Priority, Fast, and the workflow trigger as siblings in one .quick-entry-actions row (no shell/layout regression; jsdom cannot measure real pixel heights)", () => {
|
||||
mockDesktopViewport();
|
||||
renderQuickEntryBox();
|
||||
|
||||
const saveButton = screen.getByTestId("quick-entry-save");
|
||||
const attachButton = screen.getByTestId("quick-entry-attach");
|
||||
const githubToggle = screen.getByTestId("quick-entry-github-toggle");
|
||||
const priorityButton = screen.getByTestId("quick-entry-priority-button");
|
||||
const fastToggle = screen.getByTestId("quick-entry-fast-toggle");
|
||||
const workflowTrigger = screen.getByTestId("quick-entry-workflow-trigger");
|
||||
|
||||
const actionsRow = saveButton.closest(".quick-entry-actions");
|
||||
expect(actionsRow).not.toBeNull();
|
||||
expect(actionsRow?.contains(attachButton)).toBe(true);
|
||||
expect(actionsRow?.contains(githubToggle)).toBe(true);
|
||||
expect(actionsRow?.contains(priorityButton)).toBe(true);
|
||||
expect(actionsRow?.contains(fastToggle)).toBe(true);
|
||||
expect(actionsRow?.contains(workflowTrigger)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -198,11 +198,12 @@ section under one line.
|
||||
/*
|
||||
FNXC:BoardComposer 2026-07-10-12:00:
|
||||
The action row is organized as two logical clusters instead of one undifferentiated wrap of chips:
|
||||
- `.quick-entry-options-group` — task options (workflow, priority, optional steps, subtask, deps,
|
||||
models, node, agent, GitHub tracking), wrapping left-to-right with one consistent chip gap.
|
||||
- `.quick-entry-primary-group` — attach + Fast + Save; margin-left auto keeps it right-aligned so the
|
||||
distinct Save action always ends the row (and wraps as one unit on narrow columns, never splitting
|
||||
Save from its neighbors).
|
||||
- `.quick-entry-options-group` — task options (workflow, optional steps, subtask, deps,
|
||||
models, node, agent), wrapping left-to-right with one consistent chip gap.
|
||||
- `.quick-entry-primary-group` — attach + GitHub tracking + Priority + Fast + Save; margin-left auto
|
||||
keeps it right-aligned so the distinct Save action always ends the row (and wraps as one unit on
|
||||
narrow columns, never splitting Save from its neighbors). Priority and Fast are icon-only here; the
|
||||
priority glyph follows the shared up/high, down/low, flag/normal, alert/urgent mapping.
|
||||
Chip HEIGHT consistency is already enforced by the FN-7680/FN-7683 fixed-box rule on
|
||||
`.quick-entry-actions .btn` above (descendant selector — still applies inside these wrappers); these
|
||||
rules only own grouping/spacing/alignment.
|
||||
@@ -315,6 +316,14 @@ surfaces use are untouched.
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.quick-entry-primary-group .btn.btn-primary:not(.btn-task-create),
|
||||
.quick-entry-primary-group .btn.btn-primary:not(.btn-task-create):hover,
|
||||
.quick-entry-primary-group .btn.btn-primary:not(.btn-task-create):focus-visible {
|
||||
background: var(--cta-bg);
|
||||
border-color: var(--cta-border);
|
||||
color: var(--cta-text);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:QuickAddWorkflow 2026-06-30-00:00:
|
||||
Quick-add workflow targeting sits in the action row as a compact existing-button dropdown. Use tokenized sizing/colors so the selector wraps with Save/Plan controls on narrow Board and List surfaces without introducing a separate visual hierarchy.
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Task, Settings, TaskPriority, ResolvedWorkflowOptionalStep, Thinki
|
||||
import type { ModelInfo, Agent, CreateTaskInput, DuplicateMatch, BoardWorkflowDefinition, NodeInfo } from "../api";
|
||||
import { checkDuplicateTasks, fetchModels, fetchSettings, updateGlobalSettings, fetchAgents, uploadAttachment, fetchWorkflowOptionalSteps } from "../api";
|
||||
import { DuplicateWarningModal } from "./DuplicateWarningModal";
|
||||
import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot, Server, Flag } from "lucide-react";
|
||||
import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot, Server, Zap } from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
@@ -17,6 +17,7 @@ import { NodeHealthDot } from "./NodeHealthDot";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { WorkflowOptionalStepsDropdown } from "./WorkflowOptionalStepsDropdown";
|
||||
import { WorkflowIcon } from "./WorkflowIcon";
|
||||
import { getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator";
|
||||
|
||||
const STORAGE_KEY = "kb-quick-entry-text";
|
||||
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||
@@ -1636,6 +1637,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const githubToggleLabel = effectiveGithubTracking
|
||||
? t("tasks.githubTrackingOn", "GitHub tracking ON for next task (project default: {{default}})", { default: projectGithubTrackingDefault ? t("tasks.githubTrackingDefaultOn", "on") : t("tasks.githubTrackingDefaultOff", "off") })
|
||||
: t("tasks.githubTrackingOff", "GitHub tracking OFF for next task (project default: {{default}})", { default: projectGithubTrackingDefault ? t("tasks.githubTrackingDefaultOn", "on") : t("tasks.githubTrackingDefaultOff", "off") });
|
||||
const PriorityIcon = getPriorityIcon(priority);
|
||||
const priorityLabel = getPriorityLabel(priority);
|
||||
const priorityButtonLabel = t("tasks.quickEntryPriorityLabel", "Priority: {{priority}}", { priority: priorityLabel });
|
||||
const fastToggleLabel = t("tasks.toggleFastMode", "Toggle fast execution mode");
|
||||
|
||||
// Show expanded controls based on disclosure state (user preference), not textarea focus
|
||||
const showExpandedControls = isDisclosureExpanded;
|
||||
@@ -1731,10 +1736,14 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
FNXC:BoardComposer 2026-07-10-12:00:
|
||||
First-run review flagged the quick-add composer as disorganized: chips wrapped into four
|
||||
arbitrary-looking rows with Save buried mid-row. Reorganize into two logical clusters inside
|
||||
the single wrapping action row: an options group (workflow, priority, optional steps, subtask,
|
||||
deps, models, node, agent, GitHub tracking) and a right-aligned primary group (attach, Fast,
|
||||
Save) so the primary Save action always reads last/right. All triggers, popovers, and test ids
|
||||
are unchanged — this is a layout/organization pass only.
|
||||
the single wrapping action row: an options group (workflow, optional steps, subtask, deps,
|
||||
models, node, agent) and a right-aligned primary group (attach, GitHub tracking, Priority,
|
||||
Fast, Save) so status controls sit beside attach and Save still reads last/right.
|
||||
|
||||
FNXC:QuickAddActionRow 2026-07-10-21:45:
|
||||
Priority and Fast are icon-only in the bottom primary group: priority uses the shared
|
||||
up/high, down/low, flag/normal, alert/urgent glyph helper, and Fast uses Zap while retaining
|
||||
title/aria-label/test-id semantics.
|
||||
*/}
|
||||
<div className="quick-entry-options-group" data-testid="quick-entry-options-group">
|
||||
{showWorkflowSelector && (
|
||||
@@ -1837,73 +1846,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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,
|
||||
)}
|
||||
|
||||
<WorkflowOptionalStepsDropdown
|
||||
steps={optionalSteps}
|
||||
@@ -2212,28 +2154,14 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
</div>,
|
||||
portalRoot,
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${effectiveGithubTracking ? "btn-primary" : ""}`}
|
||||
onClick={() => {
|
||||
setGithubTrackingOverride((prev) => !(prev ?? projectGithubTrackingDefault));
|
||||
}}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
aria-pressed={effectiveGithubTracking}
|
||||
data-testid="quick-entry-github-toggle"
|
||||
title={githubToggleLabel}
|
||||
aria-label={githubToggleLabel}
|
||||
>
|
||||
<ProviderIcon provider="github" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
FNXC:BoardComposer 2026-07-10-12:00:
|
||||
Primary action cluster: attach + Fast sit directly beside Save, and Save is the LAST control in
|
||||
DOM order so it is right-aligned (margin-left auto on the cluster) and reads as the composer's
|
||||
primary action. Save keeps its distinct `btn-task-create` styling.
|
||||
Primary action cluster: attach + GitHub tracking + Priority + Fast sit directly beside Save,
|
||||
and Save is the LAST control in DOM order so it is right-aligned (margin-left auto on the
|
||||
cluster) and reads as the composer's primary action. Save keeps its distinct `btn-task-create`
|
||||
styling.
|
||||
FNXC:QuickAddAttachments 2026-06-30-00:00 (relocated 2026-07-10): the attachment affordance
|
||||
stays adjacent to Save (now immediately to its LEFT) preserving the icon-only label, hidden
|
||||
file input trigger, and pending-count badge.
|
||||
@@ -2254,6 +2182,90 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${effectiveGithubTracking ? "btn-primary" : ""}`}
|
||||
onClick={() => {
|
||||
setGithubTrackingOverride((prev) => !(prev ?? projectGithubTrackingDefault));
|
||||
}}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
aria-pressed={effectiveGithubTracking}
|
||||
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"
|
||||
title={priorityButtonLabel}
|
||||
aria-label={priorityButtonLabel}
|
||||
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;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PriorityIcon size={12} aria-hidden="true" />
|
||||
</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 = getPriorityLabel(taskPriority);
|
||||
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" : ""}`}
|
||||
@@ -2261,9 +2273,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
aria-pressed={isFastMode}
|
||||
data-testid="quick-entry-fast-toggle"
|
||||
title={t("tasks.toggleFastMode", "Toggle fast execution mode")}
|
||||
title={fastToggleLabel}
|
||||
aria-label={fastToggleLabel}
|
||||
>
|
||||
{t("tasks.fast", "Fast")}
|
||||
<Zap size={12} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@@ -8,8 +8,9 @@ import { applyPresetToSelection, getRecommendedPresetForSize } from "../utils/mo
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { NodeHealthDot } from "./NodeHealthDot";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
import { Sparkles, ChevronUp, ChevronDown, Maximize2, Minimize2, Paperclip, Flag, Zap, Brain, Server } from "lucide-react";
|
||||
import { Sparkles, ChevronUp, ChevronDown, Maximize2, Minimize2, Paperclip, Zap, Brain, Server } from "lucide-react";
|
||||
import { REPO_OVERRIDE_RE, resolveEffectiveGithubRepoDefault } from "./githubTracking";
|
||||
import { getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { WorkflowIcon } from "./WorkflowIcon";
|
||||
|
||||
@@ -809,6 +810,11 @@ export function TaskForm({
|
||||
const selectedNode = (nodeOptions ?? []).find((node) => node.id === nodeId);
|
||||
const nodeInlineLabel = selectedNode?.name ?? t("taskForm.nodeInlineDefault", "Node");
|
||||
const modelInlineLabel = selectedPreset?.name ?? (presetMode === "custom" ? t("taskForm.modelsCustom", "Models") : t("taskForm.modelsDefault", "Models"));
|
||||
const inlinePriority = priority ?? DEFAULT_TASK_PRIORITY;
|
||||
const InlinePriorityIcon = getPriorityIcon(inlinePriority);
|
||||
const inlinePriorityLabel = getPriorityLabel(inlinePriority);
|
||||
const inlinePriorityButtonLabel = t("taskForm.priorityInlineAria", "Priority: {{priority}}", { priority: inlinePriorityLabel });
|
||||
const inlineFastButtonLabel = t("taskForm.toggleFastMode", "Toggle fast execution mode");
|
||||
|
||||
const revealAdvancedControl = useCallback((selector: string) => {
|
||||
if (!forceMoreOptionsOpen) setShowMoreOptions(true);
|
||||
@@ -964,7 +970,10 @@ export function TaskForm({
|
||||
Common quick-add action row, adjacent to the description (create mode only). The deep/advanced controls stay collapsed behind the "Advanced" disclosure, but the buttons users reach for most — Attach, Fast (execution-mode), Priority — are surfaced INLINE here next to Plan, styled identically to QuickEntryBox's quick-add buttons (shared `.btn .btn-sm`, `.dep-trigger`, lucide icons at size 12). They are wired to TaskForm's existing state/handlers, NOT duplicated:
|
||||
- Attach → fileInputRef.click() (same hidden input the Advanced Attachments group uses; onImagesChange handles the file).
|
||||
- Fast → toggles executionMode standard⇄fast via onExecutionModeChange (mirrors QuickEntryBox quick-entry-fast-toggle).
|
||||
- Priority → cycles through TASK_PRIORITIES via onPriorityChange (Flag affordance).
|
||||
- Priority → cycles through TASK_PRIORITIES via onPriorityChange and uses the shared priorityIndicator glyph language.
|
||||
|
||||
FNXC:NewTaskDialogAffordances 2026-07-10-21:45:
|
||||
Priority and Fast are icon-only in the inline New Task row to match QuickEntryBox: priority uses the shared up/high, down/low, flag/normal, alert/urgent helper, and Fast uses Zap while title/aria-label/test-id semantics preserve accessibility and tests.
|
||||
Plan/Subtask remain gated on their handoff callbacks. Model selectors, branch/base, node, review level, and GitHub tracking stay in the Advanced disclosure.
|
||||
|
||||
FNXC:NewTaskDialogAffordances 2026-06-23-21:20:
|
||||
@@ -1047,12 +1056,12 @@ export function TaskForm({
|
||||
className={`btn btn-sm ${executionMode === "fast" ? "btn-primary" : ""}`}
|
||||
onClick={() => handleExecutionModeChange(executionMode === "fast" ? "standard" : "fast")}
|
||||
aria-pressed={executionMode === "fast"}
|
||||
aria-label={inlineFastButtonLabel}
|
||||
disabled={disabled}
|
||||
data-testid="task-form-inline-fast"
|
||||
title={t("taskForm.toggleFastMode", "Toggle fast execution mode")}
|
||||
title={inlineFastButtonLabel}
|
||||
>
|
||||
<Zap size={12} className="task-form-action-icon" />
|
||||
{t("taskForm.fast", "Fast")}
|
||||
<Zap size={12} className="task-form-action-icon" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1137,26 +1146,22 @@ export function TaskForm({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* FNXC:NewTask 2026-06-23-00:10: Priority — cycles TASK_PRIORITIES via onPriorityChange (Flag affordance, same label shape as QuickEntryBox). */}
|
||||
{/* FNXC:NewTask 2026-06-23-00:10: Priority — cycles TASK_PRIORITIES via onPriorityChange (shared icon-only glyph language, same accessible label shape as QuickEntryBox). */}
|
||||
{onPriorityChange && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={() => {
|
||||
const current = priority ?? DEFAULT_TASK_PRIORITY;
|
||||
const idx = TASK_PRIORITIES.indexOf(current);
|
||||
const idx = TASK_PRIORITIES.indexOf(inlinePriority);
|
||||
const next = TASK_PRIORITIES[(idx + 1) % TASK_PRIORITIES.length];
|
||||
onPriorityChange(next);
|
||||
}}
|
||||
aria-label={inlinePriorityButtonLabel}
|
||||
disabled={disabled}
|
||||
data-testid="task-form-inline-priority"
|
||||
title={t("taskForm.priorityLabel", "Priority")}
|
||||
title={inlinePriorityButtonLabel}
|
||||
>
|
||||
<Flag size={12} className="task-form-action-icon" />
|
||||
{(() => {
|
||||
const p = priority ?? DEFAULT_TASK_PRIORITY;
|
||||
return `${p[0].toUpperCase()}${p.slice(1)}`;
|
||||
})()}
|
||||
<InlinePriorityIcon size={12} className="task-form-action-icon" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,10 @@ vi.mock("lucide-react", () => ({
|
||||
Minimize2: () => null,
|
||||
Workflow: () => null,
|
||||
Paperclip: () => null,
|
||||
ArrowDown: () => null,
|
||||
ArrowUp: () => null,
|
||||
Flag: () => null,
|
||||
TriangleAlert: () => null,
|
||||
Zap: () => null,
|
||||
Brain: () => null,
|
||||
Server: () => null,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, act, createEvent } from "@testing-library/react";
|
||||
import { QuickEntryBox } from "../QuickEntryBox";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { TASK_PRIORITIES, type Task, type TaskPriority } from "@fusion/core";
|
||||
import { checkDuplicateTasks, fetchSettings, fetchAgents, uploadAttachment, fetchWorkflowOptionalSteps } from "../../api";
|
||||
import { useNodes } from "../../hooks/useNodes";
|
||||
import { scopedKey } from "../../utils/projectStorage";
|
||||
@@ -211,25 +211,29 @@ vi.mock("../../hooks/useNodes", () => ({
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock("lucide-react", () => {
|
||||
const MockIcon = (props: any) => <svg aria-hidden="true" {...props} />;
|
||||
const MockIcon = (iconClass: string) => ({ className, ...props }: any) => <svg aria-hidden="true" className={[iconClass, className].filter(Boolean).join(" ")} {...props} />;
|
||||
return {
|
||||
Link: MockIcon,
|
||||
Paperclip: MockIcon,
|
||||
Brain: MockIcon,
|
||||
Lightbulb: MockIcon,
|
||||
ListTree: MockIcon,
|
||||
Sparkles: MockIcon,
|
||||
Save: MockIcon,
|
||||
X: MockIcon,
|
||||
ChevronDown: MockIcon,
|
||||
ChevronUp: MockIcon,
|
||||
ChevronRight: MockIcon,
|
||||
Bot: MockIcon,
|
||||
Server: MockIcon,
|
||||
Flag: MockIcon,
|
||||
Github: MockIcon,
|
||||
Maximize2: MockIcon,
|
||||
Minimize2: MockIcon,
|
||||
Link: MockIcon("lucide-link"),
|
||||
Paperclip: MockIcon("lucide-paperclip"),
|
||||
Brain: MockIcon("lucide-brain"),
|
||||
Lightbulb: MockIcon("lucide-lightbulb"),
|
||||
ListTree: MockIcon("lucide-list-tree"),
|
||||
Sparkles: MockIcon("lucide-sparkles"),
|
||||
Save: MockIcon("lucide-save"),
|
||||
X: MockIcon("lucide-x"),
|
||||
ChevronDown: MockIcon("lucide-chevron-down"),
|
||||
ChevronUp: MockIcon("lucide-chevron-up"),
|
||||
ChevronRight: MockIcon("lucide-chevron-right"),
|
||||
Bot: MockIcon("lucide-bot"),
|
||||
Server: MockIcon("lucide-server"),
|
||||
ArrowDown: MockIcon("lucide-arrow-down"),
|
||||
ArrowUp: MockIcon("lucide-arrow-up"),
|
||||
Flag: MockIcon("lucide-flag"),
|
||||
TriangleAlert: MockIcon("lucide-triangle-alert"),
|
||||
Zap: MockIcon("lucide-zap"),
|
||||
Github: MockIcon("lucide-github"),
|
||||
Maximize2: MockIcon("lucide-maximize-2"),
|
||||
Minimize2: MockIcon("lucide-minimize-2"),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -407,23 +411,39 @@ function mockMobileViewport() {
|
||||
|
||||
/*
|
||||
FNXC:BoardComposer 2026-07-10-12:00:
|
||||
DOM order mirrors the reorganized composer action row: the options group (priority, subtask, deps,
|
||||
models, node, agent, GitHub) comes first, followed by the right-aligned primary group
|
||||
(attach, Fast, Save) with Save as the LAST control.
|
||||
DOM order mirrors the reorganized composer action row: the options group (subtask, deps,
|
||||
models, node, agent) comes first, followed by the right-aligned primary group (attach, GitHub,
|
||||
Priority, Fast, Save) with Save as the LAST control.
|
||||
*/
|
||||
const QUICK_ENTRY_ACTION_BUTTONS = [
|
||||
["Priority", "quick-entry-priority-button"],
|
||||
["Subtask", "subtask-button"],
|
||||
["Deps", "quick-entry-deps"],
|
||||
["Models", "quick-entry-models"],
|
||||
["Node", "quick-entry-node-button"],
|
||||
["Agent", "quick-entry-agent-button"],
|
||||
["GitHub", "quick-entry-github-toggle"],
|
||||
["Attach", "quick-entry-attach"],
|
||||
["GitHub", "quick-entry-github-toggle"],
|
||||
["Priority", "quick-entry-priority-button"],
|
||||
["Fast", "quick-entry-fast-toggle"],
|
||||
["Save", "quick-entry-save"],
|
||||
] as const;
|
||||
|
||||
const QUICK_ENTRY_PRIORITY_ICON_CLASS: Record<TaskPriority, string> = {
|
||||
low: "lucide-arrow-down",
|
||||
normal: "lucide-flag",
|
||||
high: "lucide-arrow-up",
|
||||
urgent: "lucide-triangle-alert",
|
||||
};
|
||||
|
||||
function expectQuickEntryPriorityButton(priority: TaskPriority) {
|
||||
const label = `${priority[0].toUpperCase()}${priority.slice(1)}`;
|
||||
const priorityButton = screen.getByTestId("quick-entry-priority-button");
|
||||
expect(priorityButton).toHaveAttribute("title", `Priority: ${label}`);
|
||||
expect(priorityButton).toHaveAttribute("aria-label", `Priority: ${label}`);
|
||||
expect(priorityButton).not.toHaveTextContent(label);
|
||||
expect(priorityButton.querySelector("svg")?.classList.contains(QUICK_ENTRY_PRIORITY_ICON_CLASS[priority])).toBe(true);
|
||||
}
|
||||
|
||||
describe("QuickEntryBox", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -853,22 +873,31 @@ describe("QuickEntryBox", () => {
|
||||
|
||||
/*
|
||||
FNXC:BoardComposer 2026-07-10-12:00:
|
||||
The primary group ends the action row as [Attach, Fast, Save]: Save is the LAST control (right-
|
||||
aligned primary action) and Attach/Fast sit immediately beside it inside the same cluster.
|
||||
The primary group ends the action row as [Attach, GitHub, Priority, Fast, Save]: Save is the LAST
|
||||
control (right-aligned primary action) and Attach/GitHub/Priority/Fast sit immediately beside it
|
||||
inside the same cluster.
|
||||
*/
|
||||
it("ends the action row with the primary group: Attach and Fast immediately before Save, Save last", () => {
|
||||
it("ends the action row with the primary group: status controls beside Attach and Save last", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
const actionButtonTestIds = getActionButtonTestIdsInDomOrder();
|
||||
expect(actionButtonTestIds.slice(-3)).toEqual(["quick-entry-attach", "quick-entry-fast-toggle", "quick-entry-save"]);
|
||||
expect(actionButtonTestIds.slice(-5)).toEqual([
|
||||
"quick-entry-attach",
|
||||
"quick-entry-github-toggle",
|
||||
"quick-entry-priority-button",
|
||||
"quick-entry-fast-toggle",
|
||||
"quick-entry-save",
|
||||
]);
|
||||
|
||||
const primaryGroup = screen.getByTestId("quick-entry-primary-group");
|
||||
for (const testId of ["quick-entry-attach", "quick-entry-fast-toggle", "quick-entry-save"]) {
|
||||
for (const testId of ["quick-entry-attach", "quick-entry-github-toggle", "quick-entry-priority-button", "quick-entry-fast-toggle", "quick-entry-save"]) {
|
||||
expect(primaryGroup.contains(screen.getByTestId(testId))).toBe(true);
|
||||
}
|
||||
const optionsGroup = screen.getByTestId("quick-entry-options-group");
|
||||
expect(optionsGroup.contains(screen.getByTestId("quick-entry-save"))).toBe(false);
|
||||
for (const testId of ["quick-entry-github-toggle", "quick-entry-priority-button", "quick-entry-save"]) {
|
||||
expect(optionsGroup.contains(screen.getByTestId(testId))).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("action buttons appear in correct DOM order after reorder", () => {
|
||||
@@ -1066,7 +1095,7 @@ describe("QuickEntryBox", () => {
|
||||
const highOption = await screen.findByTestId("quick-entry-priority-option-high");
|
||||
await touchPriorityOption(highOption);
|
||||
|
||||
expect(priorityButton.textContent).toContain("High");
|
||||
expectQuickEntryPriorityButton("high");
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("quick-entry-priority-option-normal")).toBeNull();
|
||||
});
|
||||
@@ -1259,7 +1288,7 @@ describe("QuickEntryBox", () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
expect(priorityButton.textContent).toContain("High");
|
||||
expectQuickEntryPriorityButton("high");
|
||||
expect(document.activeElement).not.toBe(textarea);
|
||||
});
|
||||
|
||||
@@ -2107,6 +2136,9 @@ describe("QuickEntryBox", () => {
|
||||
const fastToggle = screen.getByTestId("quick-entry-fast-toggle");
|
||||
expect(fastToggle).toBeTruthy();
|
||||
expect(fastToggle.getAttribute("aria-pressed")).toBe("false");
|
||||
expect(fastToggle).toHaveAttribute("aria-label", "Toggle fast execution mode");
|
||||
expect(fastToggle).not.toHaveTextContent("Fast");
|
||||
expect(fastToggle.querySelector("svg")?.classList.contains("lucide-zap")).toBe(true);
|
||||
});
|
||||
|
||||
it("shows Priority selector in expanded controls", () => {
|
||||
@@ -2115,7 +2147,18 @@ describe("QuickEntryBox", () => {
|
||||
|
||||
const priorityButton = screen.getByTestId("quick-entry-priority-button");
|
||||
expect(priorityButton).toBeTruthy();
|
||||
expect(priorityButton.textContent).toContain("Normal");
|
||||
expectQuickEntryPriorityButton("normal");
|
||||
});
|
||||
|
||||
it("renders the icon-only priority glyph and accessible label for every level", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
for (const taskPriority of TASK_PRIORITIES) {
|
||||
openPriorityMenu();
|
||||
fireEvent.click(screen.getByTestId(`quick-entry-priority-option-${taskPriority}`));
|
||||
expectQuickEntryPriorityButton(taskPriority);
|
||||
}
|
||||
});
|
||||
|
||||
it("submits selected priority through onCreate payload", async () => {
|
||||
@@ -2563,7 +2606,7 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
|
||||
expandQuickEntry();
|
||||
expect(screen.getByTestId("quick-entry-priority-button").textContent).toContain("Normal");
|
||||
expectQuickEntryPriorityButton("normal");
|
||||
});
|
||||
|
||||
it("resets priority to normal after Subtask flow", async () => {
|
||||
@@ -2582,7 +2625,7 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
|
||||
expandQuickEntry();
|
||||
expect(screen.getByTestId("quick-entry-priority-button").textContent).toContain("Normal");
|
||||
expectQuickEntryPriorityButton("normal");
|
||||
});
|
||||
|
||||
it("opens dependency dropdown when clicking deps button", () => {
|
||||
|
||||
@@ -13,7 +13,10 @@ vi.mock("lucide-react", () => ({
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
Paperclip: () => null,
|
||||
ArrowDown: () => null,
|
||||
ArrowUp: () => null,
|
||||
Flag: () => null,
|
||||
TriangleAlert: () => null,
|
||||
Zap: () => null,
|
||||
Brain: () => null,
|
||||
Server: () => null,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ArrowDown, ArrowUp, Flag, TriangleAlert } from "lucide-react";
|
||||
import { TASK_PRIORITIES, type TaskPriority } from "@fusion/core";
|
||||
import { getPriorityIcon, getPriorityLabel, priorityIndicator } from "../priorityIndicator";
|
||||
|
||||
const expectedIndicators: Record<TaskPriority, { icon: unknown; label: string }> = {
|
||||
low: { icon: ArrowDown, label: "Low" },
|
||||
normal: { icon: Flag, label: "Normal" },
|
||||
high: { icon: ArrowUp, label: "High" },
|
||||
urgent: { icon: TriangleAlert, label: "Urgent" },
|
||||
};
|
||||
|
||||
describe("priorityIndicator", () => {
|
||||
it("returns the shared glyph and label for every task priority", () => {
|
||||
for (const priority of TASK_PRIORITIES) {
|
||||
expect(priorityIndicator(priority)).toEqual(expectedIndicators[priority]);
|
||||
expect(getPriorityIcon(priority)).toBe(expectedIndicators[priority].icon);
|
||||
expect(getPriorityLabel(priority)).toBe(expectedIndicators[priority].label);
|
||||
}
|
||||
});
|
||||
});
|
||||
30
packages/dashboard/app/utils/priorityIndicator.tsx
Normal file
30
packages/dashboard/app/utils/priorityIndicator.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ArrowDown, ArrowUp, Flag, TriangleAlert, type LucideIcon } from "lucide-react";
|
||||
import type { TaskPriority } from "@fusion/core";
|
||||
|
||||
export interface PriorityIndicator {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:QuickAddPriorityIndicator 2026-07-10-12:00:
|
||||
Quick-add and New Task priority affordances must share one glyph language: ArrowUp means high, ArrowDown means low, Flag means normal, and TriangleAlert means urgent. Keep this helper as the single source so icon-only priority buttons do not drift across composer surfaces.
|
||||
*/
|
||||
const PRIORITY_INDICATORS: Record<TaskPriority, PriorityIndicator> = {
|
||||
low: { icon: ArrowDown, label: "Low" },
|
||||
normal: { icon: Flag, label: "Normal" },
|
||||
high: { icon: ArrowUp, label: "High" },
|
||||
urgent: { icon: TriangleAlert, label: "Urgent" },
|
||||
};
|
||||
|
||||
export function priorityIndicator(priority: TaskPriority): PriorityIndicator {
|
||||
return PRIORITY_INDICATORS[priority];
|
||||
}
|
||||
|
||||
export function getPriorityIcon(priority: TaskPriority): LucideIcon {
|
||||
return priorityIndicator(priority).icon;
|
||||
}
|
||||
|
||||
export function getPriorityLabel(priority: TaskPriority): string {
|
||||
return priorityIndicator(priority).label;
|
||||
}
|
||||
Reference in New Issue
Block a user