FN-7677: fix quick-add workflow dropdown button height mismatch
Aligns the quick-entry workflow-trigger dropdown button height with the neighboring Save/Fast/Subtask buttons and adds a regression test plus changeset. - QuickEntryBox.css: .quick-entry-workflow-trigger re-asserts .btn-sm's padding: 4px 10px locally so the shared global .dep-trigger padding: 3px 8px no longer shortens it by ~2px - Add regression test packages/dashboard/app/__tests__/quick-entry-workflow-trigger-height.test.tsx covering the height parity - Add changeset fn-7677-quick-add-workflow-trigger-height.md (patch, fix) - Minor doc updates in cli skill fusion references (extension-tools.md, fusion-capabilities.md) Files changed: .changeset/fn-7677-quick-add-workflow-trigger-height.md | 7 + packages/cli/skill/fusion/references/extension-tools.md | 6 +- packages/cli/skill/fusion/references/fusion-capabilities.md | 4 +- packages/dashboard/app/__tests__/quick-entry-workflow-trigger-height.test.tsx | 248 +++++++++++++++++++++ packages/dashboard/app/components/QuickEntryBox.css | 5 + 5 files changed, 266 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7677 Fusion-Task-Lineage: c8cc1b0d-f9c1-4691-a5b4-14ec8ac7e98e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7677-quick-add-workflow-trigger-height.md
Normal file
7
.changeset/fn-7677-quick-add-workflow-trigger-height.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Align the quick-add workflow dropdown button height with Save/Fast/Subtask buttons.
|
||||
category: fix
|
||||
dev: `.quick-entry-workflow-trigger` in QuickEntryBox.css now re-asserts `.btn-sm`'s `padding: 4px 10px` locally so the shared global `.dep-trigger` `padding: 3px 8px` no longer shortens it by ~2px; other `.dep-trigger` surfaces (InlineCreateCard, NewTaskModal, TaskDetailModal, TaskForm) are unaffected (FN-7677).
|
||||
@@ -104,11 +104,12 @@ Request a refinement of a completed or in-review task. Creates a new follow-up t
|
||||
|
||||
### fn_task_archive
|
||||
|
||||
Archive a task from any live column (move to archived). Archived tasks are preserved for historical reference but moved out of the main board view.
|
||||
Archive a task from any live column (move to archived). Archived tasks are preserved for historical reference but moved out of the main board view. If the task is still referenced as a lineage parent by another task, archiving is rejected unless removeLineageReferences:true is passed.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID to archive from any live column (e.g. FN-001). |
|
||||
| `removeLineageReferences` | boolean | — | When true, clear incoming lineage-parent references (child sourceParentTaskId) before archiving, so a task still referenced as a lineage parent can be archived. |
|
||||
|
||||
### fn_task_unarchive
|
||||
|
||||
@@ -120,12 +121,13 @@ Unarchive an archived task (move from archived → its restore column). Restores
|
||||
|
||||
### fn_task_delete
|
||||
|
||||
Soft-delete a task from active Fusion board views. The task row and artifacts are preserved; optional allowResurrection marks the ID for intentional recreation.
|
||||
Soft-delete a task from active Fusion board views. The task row and artifacts are preserved; optional allowResurrection marks the ID for intentional recreation. If the task is still referenced as a lineage parent by another task, deletion is rejected unless removeLineageReferences:true is passed.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID to delete (e.g. FN-001) |
|
||||
| `allowResurrection` | boolean | — | When true, mark this tombstone as explicitly reusable for future recreation. |
|
||||
| `removeLineageReferences` | boolean | — | When true, clear incoming lineage-parent references (child sourceParentTaskId) before deleting, so a task still referenced as a lineage parent can be removed. |
|
||||
|
||||
### fn_task_browse_gitlab_project_issues
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
|
||||
| `fn_task_retry` | Retry a failed task — clears the error state. Non-review failures move to todo; in-review execution failures move to todo preserving progress; in-review merge failures stay in-place for auto-merge retry. |
|
||||
| `fn_task_duplicate` | Duplicate an existing task, creating a fresh copy in planning. Copies the title and description but resets all execution state. The AI planning agent will replan the new task. |
|
||||
| `fn_task_refine` | Request a refinement of a completed or in-review task. Creates a new follow-up task in planning that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes. |
|
||||
| `fn_task_archive` | Archive a task from any live column (move to archived). Archived tasks are preserved for historical reference but moved out of the main board view. |
|
||||
| `fn_task_archive` | Archive a task from any live column (move to archived). Archived tasks are preserved for historical reference but moved out of the main board view. If the task is still referenced as a lineage parent by another task, archiving is rejected unless removeLineageReferences:true is passed. |
|
||||
| `fn_task_unarchive` | Unarchive an archived task (move from archived → its restore column). Restores to the pre-archive column when available, with active execution columns downgraded to todo. |
|
||||
| `fn_task_delete` | Soft-delete a task from active Fusion board views. The task row and artifacts are preserved; optional allowResurrection marks the ID for intentional recreation. |
|
||||
| `fn_task_delete` | Soft-delete a task from active Fusion board views. The task row and artifacts are preserved; optional allowResurrection marks the ID for intentional recreation. If the task is still referenced as a lineage parent by another task, deletion is rejected unless removeLineageReferences:true is passed. |
|
||||
| `fn_task_import_github` | Import GitHub issues as Fusion tasks. Fetches open issues from a repository and creates tasks in the planning column. Each task includes the issue title and body with a link to the source issue. |
|
||||
| `fn_task_import_github_issue` | Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the planning column with the issue title and body. |
|
||||
| `fn_task_browse_github_issues` | List open GitHub issues from a repository to browse before importing. Returns issue numbers, titles, and URLs for selection. Use with fn_task_import_github_issue to import specific issues by number. |
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { loadAllAppCss, loadStylesCss } from "../test/cssFixture";
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import { QuickEntryBox } from "../components/QuickEntryBox";
|
||||
import type { Task } from "@fusion/core";
|
||||
import type { BoardWorkflowDefinition } from "../api";
|
||||
import { fetchAgents } from "../api";
|
||||
|
||||
/*
|
||||
FNXC:QuickAddWorkflow 2026-07-08-00:00:
|
||||
FN-7677 regression coverage. The workflow trigger shares `.dep-trigger` with
|
||||
InlineCreateCard/NewTaskModal/TaskDetailModal/TaskForm, whose global rule sets
|
||||
`padding: 3px 8px` and overrides `.btn-sm`'s `padding: 4px 10px`. That made the
|
||||
quick-add trigger ~2px shorter than the sibling Save/Fast/Subtask `.btn.btn-sm`
|
||||
buttons. These tests assert the local `.quick-entry-workflow-trigger` override
|
||||
re-asserts `.btn-sm`'s own padding value (not a new hardcoded literal) so the
|
||||
box heights resolve equal, and that the fix holds at desktop widths (not just
|
||||
inside the mobile touch-target `min-height` media block), and that the shared
|
||||
global `.dep-trigger` rule itself remains untouched for other surfaces.
|
||||
*/
|
||||
|
||||
const mockTasks: Task[] = [
|
||||
{
|
||||
id: "FN-001",
|
||||
title: "Test task 1",
|
||||
description: "First test task",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
const WORKFLOW_A: BoardWorkflowDefinition = {
|
||||
id: "builtin:coding",
|
||||
name: "Coding",
|
||||
columns: [],
|
||||
};
|
||||
|
||||
const WORKFLOW_B: BoardWorkflowDefinition = {
|
||||
id: "wf-custom-long-name",
|
||||
name: "A Rather Long Custom Workflow Name That Should Truncate",
|
||||
columns: [],
|
||||
};
|
||||
|
||||
vi.mock("../api", () => ({
|
||||
fetchModels: vi.fn().mockResolvedValue({
|
||||
models: [],
|
||||
favoriteProviders: [],
|
||||
favoriteModels: [],
|
||||
}),
|
||||
fetchSettings: vi.fn().mockResolvedValue({
|
||||
modelPresets: [],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 30000,
|
||||
groupOverlappingFiles: true,
|
||||
autoMerge: true,
|
||||
}),
|
||||
fetchAgents: vi.fn().mockResolvedValue([]),
|
||||
fetchWorkflowOptionalSteps: vi.fn().mockResolvedValue([]),
|
||||
uploadAttachment: vi.fn().mockResolvedValue({}),
|
||||
updateGlobalSettings: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
Link: () => null,
|
||||
Paperclip: () => null,
|
||||
Brain: () => null,
|
||||
Lightbulb: () => null,
|
||||
ListTree: () => null,
|
||||
Sparkles: () => null,
|
||||
Save: () => null,
|
||||
X: () => null,
|
||||
ChevronDown: () => null,
|
||||
ChevronUp: () => null,
|
||||
ChevronRight: () => null,
|
||||
Bot: () => null,
|
||||
Server: () => null,
|
||||
Flag: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../components/ModelSelectionModal", () => ({
|
||||
ModelSelectionModal: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../components/CustomModelDropdown", () => ({
|
||||
CustomModelDropdown: ({
|
||||
value,
|
||||
label,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
label: string;
|
||||
}) => <div data-testid={`mock-dropdown-${label}`}>{value || "none"}</div>,
|
||||
}));
|
||||
|
||||
function mockDesktopViewport() {
|
||||
Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
|
||||
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
function renderQuickEntryBox(props: Record<string, unknown> = {}) {
|
||||
const defaultProps = {
|
||||
onCreate: vi.fn().mockResolvedValue(undefined),
|
||||
addToast: vi.fn(),
|
||||
tasks: mockTasks,
|
||||
projectId: "test-proj",
|
||||
workflowId: WORKFLOW_A.id,
|
||||
defaultWorkflowId: WORKFLOW_A.id,
|
||||
workflowOptions: [WORKFLOW_A, WORKFLOW_B],
|
||||
};
|
||||
return render(<QuickEntryBox {...defaultProps} {...props} />);
|
||||
}
|
||||
|
||||
describe("quick-entry-workflow-trigger height parity (FN-7677)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
localStorage.clear();
|
||||
vi.mocked(fetchAgents).mockResolvedValue([]);
|
||||
Object.defineProperty(URL, "createObjectURL", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn(() => "blob:mock"),
|
||||
});
|
||||
Object.defineProperty(URL, "revokeObjectURL", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
});
|
||||
vi.useRealTimers();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("renders the workflow trigger and Save button as siblings in the same action row when ≥2 workflow options exist", () => {
|
||||
mockDesktopViewport();
|
||||
renderQuickEntryBox();
|
||||
|
||||
const trigger = screen.getByTestId("quick-entry-workflow-trigger");
|
||||
const saveButton = screen.getByTestId("quick-entry-save");
|
||||
|
||||
expect(trigger.classList.contains("btn")).toBe(true);
|
||||
expect(trigger.classList.contains("btn-sm")).toBe(true);
|
||||
expect(trigger.classList.contains("dep-trigger")).toBe(true);
|
||||
expect(saveButton.classList.contains("btn")).toBe(true);
|
||||
expect(saveButton.classList.contains("btn-sm")).toBe(true);
|
||||
|
||||
// Both controls must live in the same .quick-entry-actions row.
|
||||
const actionsRow = trigger.closest(".quick-entry-actions");
|
||||
expect(actionsRow).not.toBeNull();
|
||||
expect(actionsRow?.contains(saveButton)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not render a workflow trigger when fewer than 2 real workflow options exist (no layout regression)", () => {
|
||||
mockDesktopViewport();
|
||||
renderQuickEntryBox({ workflowOptions: [WORKFLOW_A] });
|
||||
|
||||
expect(screen.queryByTestId("quick-entry-workflow-trigger")).toBeNull();
|
||||
expect(screen.getByTestId("quick-entry-save")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("re-asserts .btn-sm's own padding on .quick-entry-workflow-trigger instead of inheriting the shorter shared .dep-trigger padding", () => {
|
||||
const cssContent = loadAllAppCss();
|
||||
|
||||
// .btn-sm establishes the padding contract the Save/Fast/Subtask buttons resolve.
|
||||
const btnSmMatch = cssContent.match(/\.btn-sm\s*\{[^}]*padding:\s*([^;]+);/);
|
||||
expect(btnSmMatch).not.toBeNull();
|
||||
const btnSmPadding = btnSmMatch![1].trim();
|
||||
expect(btnSmPadding).toBe("4px 10px");
|
||||
|
||||
// The shared global .dep-trigger rule sets a shorter padding and MUST remain
|
||||
// untouched — other surfaces (InlineCreateCard, NewTaskModal, TaskDetailModal,
|
||||
// TaskForm) still depend on its 3px/8px sizing.
|
||||
const depTriggerMatch = cssContent.match(
|
||||
/\.dep-trigger,\s*\n\s*\.inline-create-model-trigger\s*\{[^}]*padding:\s*([^;]+);/,
|
||||
);
|
||||
expect(depTriggerMatch).not.toBeNull();
|
||||
expect(depTriggerMatch![1].trim()).toBe("3px 8px");
|
||||
|
||||
// .quick-entry-workflow-trigger must locally re-assert the .btn-sm padding
|
||||
// value (not a new arbitrary literal) so cascade order resolves it to the
|
||||
// same box height as its .btn.btn-sm siblings.
|
||||
const triggerMatch = cssContent.match(
|
||||
/\.quick-entry-workflow-trigger\s*\{[^}]*padding:\s*([^;]+);/,
|
||||
);
|
||||
expect(triggerMatch).not.toBeNull();
|
||||
const triggerPadding = triggerMatch![1].trim();
|
||||
expect(triggerPadding).toBe(btnSmPadding);
|
||||
expect(triggerPadding).not.toBe(depTriggerMatch![1].trim());
|
||||
});
|
||||
|
||||
it("keeps the height-parity override in the base (non-media-query) rule so desktop widths are covered too, not only the ≤768px touch-target block", () => {
|
||||
const cssContent = loadAllAppCss();
|
||||
|
||||
// Strip everything inside @media blocks to isolate base/desktop rules.
|
||||
const withoutMediaBlocks = cssContent.replace(/@media[^{]*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
|
||||
|
||||
const baseTriggerMatch = withoutMediaBlocks.match(
|
||||
/\.quick-entry-workflow-trigger\s*\{[^}]*padding:\s*([^;]+);/,
|
||||
);
|
||||
expect(baseTriggerMatch).not.toBeNull();
|
||||
expect(baseTriggerMatch![1].trim()).toBe("4px 10px");
|
||||
});
|
||||
|
||||
it("does not modify the shared global .dep-trigger rule's selector list (InlineCreateCard/NewTaskModal/TaskDetailModal/TaskForm still share it)", () => {
|
||||
const stylesCssContent = loadStylesCss();
|
||||
const depTriggerMatches = stylesCssContent.match(/\.dep-trigger,\s*\n\s*\.inline-create-model-trigger\s*\{/g);
|
||||
expect(depTriggerMatches).not.toBeNull();
|
||||
expect(depTriggerMatches!.length).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps the workflow icon, truncating label, and chevron intact when the trigger renders (long label)", () => {
|
||||
mockDesktopViewport();
|
||||
renderQuickEntryBox({ workflowOptions: [WORKFLOW_A, WORKFLOW_B], workflowId: WORKFLOW_B.id, defaultWorkflowId: WORKFLOW_B.id });
|
||||
|
||||
const trigger = screen.getByTestId("quick-entry-workflow-trigger");
|
||||
const label = trigger.querySelector(".quick-entry-workflow-label");
|
||||
expect(label).not.toBeNull();
|
||||
expect(label?.textContent).toBeTruthy();
|
||||
// Chevron rendered via mocked lucide-react (ChevronDown -> null), so assert
|
||||
// the trigger still has non-empty content (icon slot + label), i.e. it is
|
||||
// not an empty shell.
|
||||
expect(trigger.textContent?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -209,6 +209,10 @@ The wide menu is portaled and fixed-positioned by QuickEntryBox so right-side bo
|
||||
max-width: min(calc(var(--space-xl) * 5), 100%);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:QuickAddWorkflow 2026-07-08-00:00:
|
||||
FN-7677 — the workflow trigger shares `.btn.btn-sm.dep-trigger` classes with other surfaces (InlineCreateCard, NewTaskModal, TaskDetailModal, TaskForm), but the shared `.dep-trigger, .inline-create-model-trigger` rule in styles.css sets `padding: 3px 8px`, overriding `.btn-sm`'s `padding: 4px 10px` and making this trigger ~2px shorter than its Save/Fast/Subtask `.btn.btn-sm` siblings in `.quick-entry-actions`. Re-assert `.btn-sm`'s own padding value scoped to this selector only (do not touch the shared global `.dep-trigger` rule — other surfaces still depend on its 3px/8px sizing) so the quick-add action row reads as one uniform height.
|
||||
*/
|
||||
.quick-entry-workflow-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -217,6 +221,7 @@ The wide menu is portaled and fixed-positioned by QuickEntryBox so right-side bo
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: min(calc(var(--space-xl) * 5), calc(100vw - var(--space-lg)));
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.quick-entry-workflow-icon {
|
||||
|
||||
Reference in New Issue
Block a user