FN-8194: align task detail inline controls
Align task-detail actions with Quick Add while preserving existing integrations. - Add inline attachment and eligible GitHub tracking controls - Replace the Oversight menu dots with an Eye icon and reorder metadata actions - Cover action order, integration behavior, icon rendering, and mobile wrapping Files changed: .changeset/fn-8194-task-detail-inline-controls.md | 7 ++ docs/dashboard-guide.md | 6 +- .../dashboard/app/components/TaskDetailModal.tsx | 99 +++++++++++++++------- ...lModal.inline-editing-and-integrations.test.tsx | 80 ++++++++++++++++- .../TaskDetailModal.mock-coverage.test.ts | 1 + .../TaskDetailModal.oversight-controls.test.tsx | 19 +++++ ...etailModal.responsive-and-dependencies.test.tsx | 5 +- .../__tests__/TaskDetailModal.test-helpers.ts | 10 +-- 8 files changed, 187 insertions(+), 40 deletions(-) Fusion-Task-Id: FN-8194 Fusion-Task-Lineage: 8de0e36f-b428-401c-98aa-35964c556a39 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8194-task-detail-inline-controls.md
Normal file
7
.changeset/fn-8194-task-detail-inline-controls.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Task detail action row now matches Quick Add — Eye icon for oversight, plus attach and GitHub-tracking buttons.
|
||||||
|
category: feature
|
||||||
|
dev: TaskDetailModal inline controls reordered to attach → GitHub → oversight(Eye) → priority → Fast; reuses existing upload and GitHub-tracking handlers. New test ids: detail-inline-attach, detail-inline-github-toggle.
|
||||||
@@ -232,8 +232,10 @@ Features:
|
|||||||
- Task cards show a compact **planner-overseer eye badge** (`data-testid="planner-overseer-state-badge"`) when the engine reports a non-idle transient `plannerOverseerState`. The eye badge is an active-overseer state marker, not a human-read/view indicator: `watching` means passive monitoring, `steering`/`recovering` mean active guidance or recovery is underway, and `awaiting-confirmation` means a human decision is required before the overseer can continue. Hover exposes the composed tooltip with the overseer's reason, watched stage/signal, and pending-confirmation note when present.
|
- Task cards show a compact **planner-overseer eye badge** (`data-testid="planner-overseer-state-badge"`) when the engine reports a non-idle transient `plannerOverseerState`. The eye badge is an active-overseer state marker, not a human-read/view indicator: `watching` means passive monitoring, `steering`/`recovering` mean active guidance or recovery is underway, and `awaiting-confirmation` means a human decision is required before the overseer can continue. Hover exposes the composed tooltip with the overseer's reason, watched stage/signal, and pending-confirmation note when present.
|
||||||
<!-- FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 adds interactive task-detail planner-overseer controls (quick level change, manual nudge, stop oversight, explain current action) alongside the FN-7516 read-only card badges above. These controls live ONLY in TaskDetailModal, not TaskCard.
|
<!-- FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 adds interactive task-detail planner-overseer controls (quick level change, manual nudge, stop oversight, explain current action) alongside the FN-7516 read-only card badges above. These controls live ONLY in TaskDetailModal, not TaskCard.
|
||||||
|
|
||||||
FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 collapses the desktop inline cluster (documented below through FN-7545/FN-7546) into the single universal overflow-menu dropdown that FN-7545 originally built for mobile only — the dropdown is now the ONE canonical surface on every viewport, desktop included. -->
|
FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 collapses the desktop inline cluster (documented below through FN-7545/FN-7546) into the single universal overflow-menu dropdown that FN-7545 originally built for mobile only — the dropdown is now the ONE canonical surface on every viewport, desktop included.
|
||||||
- The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) exposes planner-overseer controls behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` icon, `aria-haspopup="menu"`) on every viewport, desktop and mobile alike. Clicking the trigger opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) containing: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes. The trigger itself is withheld entirely when none of the menu's controls would render (no empty-shell overflow button for the unresolved-default case).
|
|
||||||
|
FNXC:QuickAddActionRow 2026-07-16-16:00: FN-8194 aligns task-detail metadata actions with Quick Add: attach, GitHub tracking, Oversight (Eye), Priority, then Fast. -->
|
||||||
|
- The task detail modal's inline meta-controls cluster follows Quick Add order: compact **Attach file** (`data-testid="detail-inline-attach"`), eligible-task **GitHub tracking** toggle (`data-testid="detail-inline-github-toggle"`), **Oversight**, Priority, and Execution mode. Attach opens the existing task attachment picker; GitHub toggles the existing tracking setting and is omitted for GitLab-tracked or non-editable tasks. The Oversight control exposes planner-overseer actions behind a compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `Eye` icon, `aria-haspopup="menu"`) on every viewport, desktop and mobile alike. Clicking the trigger opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) containing: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes. The trigger itself is withheld entirely when none of the menu's controls would render (no empty-shell overflow button for the unresolved-default case).
|
||||||
<!-- FNXC:PlannerOversight 2026-07-04-20:30: FN-7546 clarifies the cluster above — operators reported the buttons were unlabeled and looked inert, with only a hover title explaining why. Adds a visible group label and an always-visible disabled-reason line, and makes Explain always openable since it never mutates anything. -->
|
<!-- FNXC:PlannerOversight 2026-07-04-20:30: FN-7546 clarifies the cluster above — operators reported the buttons were unlabeled and looked inert, with only a hover title explaining why. Adds a visible group label and an always-visible disabled-reason line, and makes Explain always openable since it never mutates anything. -->
|
||||||
- The controls inside the Oversight menu carry a visible, non-interactive **`"Overseer controls"` group label** (`data-testid="detail-oversight-controls-label"`) so Nudge/Stop/Explain read as an identifiable cluster rather than unlabeled entries; the label is gated by the same `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff` condition as the buttons, so it never renders when oversight is Off/unresolved (opening the menu in that state shows only the level select). When **Nudge** is disabled, an always-visible helper line (`data-testid="detail-overseer-nudge-disabled-reason"`) states the reason in-DOM (mirroring the existing hover `title`) instead of relying on a mouse-hover tooltip alone. **Explain** is read-only and non-mutating, so it is never disabled purely because the overseer is inactive — clicking it always opens/closes the panel, which shows the overseer's live state when watching or an informative "not currently watching this task" message otherwise. Nudge's mutating enablement rule (`canNudgeOverseer`, including the human-control suppression cases) and Stop's confirmation dialog are unchanged.
|
- The controls inside the Oversight menu carry a visible, non-interactive **`"Overseer controls"` group label** (`data-testid="detail-oversight-controls-label"`) so Nudge/Stop/Explain read as an identifiable cluster rather than unlabeled entries; the label is gated by the same `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff` condition as the buttons, so it never renders when oversight is Off/unresolved (opening the menu in that state shows only the level select). When **Nudge** is disabled, an always-visible helper line (`data-testid="detail-overseer-nudge-disabled-reason"`) states the reason in-DOM (mirroring the existing hover `title`) instead of relying on a mouse-hover tooltip alone. **Explain** is read-only and non-mutating, so it is never disabled purely because the overseer is inactive — clicking it always opens/closes the panel, which shows the overseer's live state when watching or an informative "not currently watching this task" message otherwise. Nudge's mutating enablement rule (`canNudgeOverseer`, including the human-control suppression cases) and Stop's confirmation dialog are unchanged.
|
||||||
<!-- FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 originally introduced this overflow menu for mobile only, collapsing the level-select/nudge/stop/explain controls behind the trigger below the 768px breakpoint while desktop kept an inline cluster.
|
<!-- FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 originally introduced this overflow menu for mobile only, collapsing the level-select/nudge/stop/explain controls behind the trigger below the 768px breakpoint while desktop kept an inline cluster.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "./TaskDetailModal.css";
|
|||||||
import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2, Send, Square, Info, MoreVertical, Eye, EyeOff } from "lucide-react";
|
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2, Send, Square, Info, Paperclip, Eye, EyeOff } from "lucide-react";
|
||||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||||
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
||||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||||
@@ -4181,40 +4181,39 @@ export function TaskDetailContent({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="detail-meta">
|
<div className="detail-meta">
|
||||||
|
{/*
|
||||||
|
FNXC:QuickAddActionRow 2026-07-16-16:00:
|
||||||
|
FN-8194: task-detail metadata mirrors Quick Add's action order:
|
||||||
|
attach, GitHub tracking, Oversight, Priority, then Fast. The compact
|
||||||
|
controls delegate to the existing single file-input upload and
|
||||||
|
GitHub-tracking handlers so this row never forks persistence paths.
|
||||||
|
*/}
|
||||||
<div className="detail-meta-inline-controls" data-testid="detail-meta-inline-controls">
|
<div className="detail-meta-inline-controls" data-testid="detail-meta-inline-controls">
|
||||||
<label
|
|
||||||
className={`card-priority-badge card-priority-badge--${inlinePriority} detail-priority-chip ${isSavingInlinePriority ? "detail-priority-chip--saving" : ""}`}
|
|
||||||
>
|
|
||||||
<span>{t("taskDetail.priority.label", "Priority:")}</span>
|
|
||||||
<select
|
|
||||||
className="detail-priority-select"
|
|
||||||
value={inlinePriority}
|
|
||||||
onChange={(event) => {
|
|
||||||
void handleInlinePriorityChange(event.target.value);
|
|
||||||
}}
|
|
||||||
disabled={isSavingInlinePriority}
|
|
||||||
aria-label={t("taskDetail.priority.ariaLabel", "Task priority")}
|
|
||||||
>
|
|
||||||
{TASK_PRIORITIES.map((priorityOption) => (
|
|
||||||
<option key={priorityOption} value={priorityOption}>
|
|
||||||
{priorityOption}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm detail-execution-mode-toggle ${inlineExecutionMode === "fast" ? "detail-execution-mode-toggle--fast" : ""} ${isSavingInlineExecutionMode ? "detail-execution-mode-toggle--saving" : ""}`}
|
className="btn btn-icon btn-sm"
|
||||||
onClick={() => {
|
data-testid="detail-inline-attach"
|
||||||
void handleInlineExecutionModeToggle();
|
onClick={() => fileInputRef.current?.click()}
|
||||||
}}
|
disabled={uploading}
|
||||||
disabled={isSavingInlineExecutionMode}
|
aria-label={t("taskDetail.attachments.attachInline", "Attach file")}
|
||||||
aria-label={t("taskDetail.executionMode.ariaLabel", "Execution mode: {{mode}}", { mode: inlineExecutionMode })}
|
title={t("taskDetail.attachments.attachInline", "Attach file")}
|
||||||
aria-pressed={inlineExecutionMode === "fast"}
|
|
||||||
>
|
>
|
||||||
<Zap aria-hidden="true" />
|
<Paperclip aria-hidden="true" />
|
||||||
<span>{inlineExecutionMode === "fast" ? t("taskDetail.executionMode.fast", "Fast") : t("taskDetail.executionMode.standard", "Standard")}</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
{canEditGithubTracking && !gitlabTrackedItem && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-icon btn-sm ${githubTrackingEnabled ? "btn-primary" : ""}`}
|
||||||
|
data-testid="detail-inline-github-toggle"
|
||||||
|
onClick={() => void handleToggleGithubTracking()}
|
||||||
|
disabled={isSavingGithubTracking}
|
||||||
|
aria-pressed={githubTrackingEnabled}
|
||||||
|
aria-label={t("taskDetail.githubTracking.toggleInline", "Toggle GitHub tracking")}
|
||||||
|
title={t("taskDetail.githubTracking.toggleInline", "Toggle GitHub tracking")}
|
||||||
|
>
|
||||||
|
<ProviderIcon provider="github" size="sm" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/*
|
{/*
|
||||||
FNXC:PlannerOversight 2026-07-04-17:00:
|
FNXC:PlannerOversight 2026-07-04-17:00:
|
||||||
FN-7517 quick oversight-level-change control. Shows the current
|
FN-7517 quick oversight-level-change control. Shows the current
|
||||||
@@ -4240,6 +4239,11 @@ export function TaskDetailContent({
|
|||||||
stop / explain) render ONLY behind the "Oversight" overflow-menu
|
stop / explain) render ONLY behind the "Oversight" overflow-menu
|
||||||
trigger on every surface (desktop and mobile); the former desktop
|
trigger on every surface (desktop and mobile); the former desktop
|
||||||
inline cluster was removed for a consistent, simpler control bar.
|
inline cluster was removed for a consistent, simpler control bar.
|
||||||
|
|
||||||
|
FNXC:PlannerOversight 2026-07-16-16:00:
|
||||||
|
FN-8194: use Eye for the Oversight overflow trigger so task detail
|
||||||
|
matches Quick Add's planner-advisor affordance without changing
|
||||||
|
the labeled menu's accessibility or behavior.
|
||||||
*/}
|
*/}
|
||||||
{(hasTaskOversightOverride || workflowOversightResolved) && (
|
{(hasTaskOversightOverride || workflowOversightResolved) && (
|
||||||
<div className="detail-oversight-menu-dropdown" ref={oversightMenuRef}>
|
<div className="detail-oversight-menu-dropdown" ref={oversightMenuRef}>
|
||||||
@@ -4254,7 +4258,7 @@ export function TaskDetailContent({
|
|||||||
aria-expanded={showOversightMenu}
|
aria-expanded={showOversightMenu}
|
||||||
aria-label={t("taskDetail.oversight.menuAriaLabel", "Oversight actions")}
|
aria-label={t("taskDetail.oversight.menuAriaLabel", "Oversight actions")}
|
||||||
>
|
>
|
||||||
<MoreVertical aria-hidden="true" />
|
<Eye aria-hidden="true" />
|
||||||
<span>{t("taskDetail.oversight.menuLabel", "Oversight")}</span>
|
<span>{t("taskDetail.oversight.menuLabel", "Oversight")}</span>
|
||||||
</button>
|
</button>
|
||||||
{showOversightMenu && (
|
{showOversightMenu && (
|
||||||
@@ -4404,6 +4408,39 @@ export function TaskDetailContent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<label
|
||||||
|
className={`card-priority-badge card-priority-badge--${inlinePriority} detail-priority-chip ${isSavingInlinePriority ? "detail-priority-chip--saving" : ""}`}
|
||||||
|
>
|
||||||
|
<span>{t("taskDetail.priority.label", "Priority:")}</span>
|
||||||
|
<select
|
||||||
|
className="detail-priority-select"
|
||||||
|
value={inlinePriority}
|
||||||
|
onChange={(event) => {
|
||||||
|
void handleInlinePriorityChange(event.target.value);
|
||||||
|
}}
|
||||||
|
disabled={isSavingInlinePriority}
|
||||||
|
aria-label={t("taskDetail.priority.ariaLabel", "Task priority")}
|
||||||
|
>
|
||||||
|
{TASK_PRIORITIES.map((priorityOption) => (
|
||||||
|
<option key={priorityOption} value={priorityOption}>
|
||||||
|
{priorityOption}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-sm detail-execution-mode-toggle ${inlineExecutionMode === "fast" ? "detail-execution-mode-toggle--fast" : ""} ${isSavingInlineExecutionMode ? "detail-execution-mode-toggle--saving" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
void handleInlineExecutionModeToggle();
|
||||||
|
}}
|
||||||
|
disabled={isSavingInlineExecutionMode}
|
||||||
|
aria-label={t("taskDetail.executionMode.ariaLabel", "Execution mode: {{mode}}", { mode: inlineExecutionMode })}
|
||||||
|
aria-pressed={inlineExecutionMode === "fast"}
|
||||||
|
>
|
||||||
|
<Zap aria-hidden="true" />
|
||||||
|
<span>{inlineExecutionMode === "fast" ? t("taskDetail.executionMode.fast", "Fast") : t("taskDetail.executionMode.standard", "Standard")}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{overseerExplainOpen && (
|
{overseerExplainOpen && (
|
||||||
<div className="detail-overseer-explain-panel" data-testid="detail-overseer-explain-panel" role="region" aria-live="polite">
|
<div className="detail-overseer-explain-panel" data-testid="detail-overseer-explain-panel" role="region" aria-live="polite">
|
||||||
|
|||||||
@@ -1207,7 +1207,12 @@ describe("TaskDetailModal", () => {
|
|||||||
const prioritySelect = screen.getByRole("combobox", { name: "Task priority" });
|
const prioritySelect = screen.getByRole("combobox", { name: "Task priority" });
|
||||||
const executionModeToggle = screen.getByRole("button", { name: "Execution mode: standard" });
|
const executionModeToggle = screen.getByRole("button", { name: "Execution mode: standard" });
|
||||||
|
|
||||||
expect(prioritySelect.parentElement).toBe(controls.firstElementChild);
|
/*
|
||||||
|
FNXC:QuickAddActionRow 2026-07-16-16:00:
|
||||||
|
FN-8194: attach, GitHub, and Oversight precede the Quick Add-matched
|
||||||
|
Priority/Fast controls; both controls remain direct interactive children.
|
||||||
|
*/
|
||||||
|
expect(controls).toContainElement(prioritySelect.parentElement);
|
||||||
expect(executionModeToggle.parentElement).toBe(controls);
|
expect(executionModeToggle.parentElement).toBe(controls);
|
||||||
|
|
||||||
fireEvent.change(prioritySelect, {
|
fireEvent.change(prioritySelect, {
|
||||||
@@ -3338,3 +3343,76 @@ describe("TaskDetailModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("TaskDetailModal inline action row parity (FN-8194)", () => {
|
||||||
|
const renderDetail = (task: Task) => render(
|
||||||
|
<TaskDetailModal
|
||||||
|
initialTab="definition"
|
||||||
|
task={task}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
it("uses the existing file input and preserves Quick Add action order", async () => {
|
||||||
|
renderDetail(makeTask({ id: "FN-8194", column: "todo", plannerOversightLevel: "observe" }));
|
||||||
|
|
||||||
|
const controls = screen.getByTestId("detail-meta-inline-controls");
|
||||||
|
const attach = screen.getByTestId("detail-inline-attach");
|
||||||
|
const github = screen.getByTestId("detail-inline-github-toggle");
|
||||||
|
const oversight = await screen.findByTestId("detail-oversight-menu-trigger");
|
||||||
|
const priority = screen.getByRole("combobox", { name: "Task priority" }).parentElement!;
|
||||||
|
const fast = screen.getByRole("button", { name: "Execution mode: standard" });
|
||||||
|
const fileInput = document.querySelector<HTMLInputElement>('input[type="file"]')!;
|
||||||
|
const fileInputClick = vi.spyOn(fileInput, "click");
|
||||||
|
|
||||||
|
expect([...controls.children]).toEqual([attach, github, oversight.parentElement, priority, fast]);
|
||||||
|
fireEvent.click(attach);
|
||||||
|
expect(fileInputClick).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles GitHub tracking through the existing update path and reflects enabled state", async () => {
|
||||||
|
const { updateTask } = await import("../../api");
|
||||||
|
const mockUpdate = vi.mocked(updateTask);
|
||||||
|
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-8194", column: "todo", githubTracking: { enabled: true } }) as Task);
|
||||||
|
|
||||||
|
renderDetail(makeTask({ id: "FN-8194", column: "todo", githubTracking: { enabled: false } }));
|
||||||
|
|
||||||
|
const toggle = screen.getByTestId("detail-inline-github-toggle");
|
||||||
|
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
||||||
|
expect(toggle).not.toHaveClass("btn-primary");
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdate).toHaveBeenCalledWith("FN-8194", { githubTracking: { enabled: true } }, undefined);
|
||||||
|
});
|
||||||
|
expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||||
|
expect(toggle).toHaveClass("btn-primary");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the GitHub toggle for non-editable and GitLab-tracked tasks", () => {
|
||||||
|
const { unmount } = renderDetail(makeTask({ id: "FN-8194", column: "done", githubTracking: { enabled: true } }));
|
||||||
|
expect(screen.queryByTestId("detail-inline-github-toggle")).not.toBeInTheDocument();
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
renderDetail(makeTask({
|
||||||
|
id: "FN-8195",
|
||||||
|
column: "todo",
|
||||||
|
gitlabTracking: {
|
||||||
|
item: {
|
||||||
|
kind: "project_issue",
|
||||||
|
projectPath: "acme/app",
|
||||||
|
iid: 1,
|
||||||
|
title: "GitLab issue",
|
||||||
|
url: "https://gitlab.com/acme/app/-/issues/1",
|
||||||
|
state: "opened",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) as Task);
|
||||||
|
expect(screen.queryByTestId("detail-inline-github-toggle")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe("TaskDetailModal lucide mock coverage", () => {
|
|||||||
expect(lucideMock.ChevronRight).toBeDefined();
|
expect(lucideMock.ChevronRight).toBeDefined();
|
||||||
expect(lucideMock.ChevronUp).toBeDefined();
|
expect(lucideMock.ChevronUp).toBeDefined();
|
||||||
expect(lucideMock.Maximize2).toBeDefined();
|
expect(lucideMock.Maximize2).toBeDefined();
|
||||||
|
expect(lucideMock.Paperclip).toBeDefined();
|
||||||
expect(lucideMock.Pencil).toBeDefined();
|
expect(lucideMock.Pencil).toBeDefined();
|
||||||
expect(lucideMock.Workflow).toBeDefined();
|
expect(lucideMock.Workflow).toBeDefined();
|
||||||
expect(lucideMock.X).toBeDefined();
|
expect(lucideMock.X).toBeDefined();
|
||||||
|
|||||||
@@ -59,6 +59,25 @@ async function openOversightMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("TaskDetailModal oversight controls", () => {
|
describe("TaskDetailModal oversight controls", () => {
|
||||||
|
it("uses the Eye icon for the labeled oversight overflow trigger", async () => {
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask({ id: "FN-8194", column: "in-progress", plannerOversightLevel: "observe" })}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const trigger = await screen.findByTestId("detail-oversight-menu-trigger");
|
||||||
|
expect(trigger).toHaveTextContent("Oversight");
|
||||||
|
expect(trigger.querySelector('[data-testid="eye-icon"]')).toBeInTheDocument();
|
||||||
|
expect(trigger.querySelector('[data-testid="more-vertical-icon"]')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
|||||||
@@ -242,11 +242,14 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
|
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps inline metadata controls in a single row without a narrow-screen column fallback", () => {
|
it("keeps inline metadata controls in a single row with a wrapping mobile fallback", () => {
|
||||||
const css = readDashboardStylesSource();
|
const css = readDashboardStylesSource();
|
||||||
|
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-meta-inline-controls");
|
||||||
|
|
||||||
expectBaseRule(css, ".detail-meta-inline-controls", "display: flex;");
|
expectBaseRule(css, ".detail-meta-inline-controls", "display: flex;");
|
||||||
expectBaseRule(css, ".detail-meta-inline-controls", "flex-wrap: nowrap;");
|
expectBaseRule(css, ".detail-meta-inline-controls", "flex-wrap: nowrap;");
|
||||||
|
expect(mobileBlock).toMatch(/\.detail-meta-inline-controls\s*\{[^}]*flex-wrap:\s*wrap;/);
|
||||||
|
expect(mobileBlock).not.toMatch(/\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
|
||||||
expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
|
expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -132,13 +132,13 @@ vi.mock("lucide-react", () => ({
|
|||||||
Code2: () => null,
|
Code2: () => null,
|
||||||
Cpu: () => null,
|
Cpu: () => null,
|
||||||
Bell: () => null,
|
Bell: () => null,
|
||||||
// FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 mobile oversight overflow-menu trigger icon.
|
|
||||||
MoreVertical: (props: any) => React.createElement("svg", { "data-testid": "more-vertical-icon", ...props }),
|
|
||||||
/*
|
/*
|
||||||
FNXC:DashboardTests 2026-07-15-11:55:
|
FNXC:DashboardTests 2026-07-16-16:00:
|
||||||
Session-advisor toggle on TaskDetailContent uses Eye/EyeOff; missing exports break every
|
FN-8194 replaces the Task Detail oversight dots with Eye and adds Paperclip
|
||||||
TaskDetailModal suite that mounts the session-advisor control (including oversight-mobile).
|
to its inline action row. Keep both exports in the shared modal mock so every
|
||||||
|
focused TaskDetailModal suite mounts the production affordances.
|
||||||
*/
|
*/
|
||||||
|
Paperclip: (props: any) => React.createElement("svg", { "data-testid": "paperclip-icon", ...props }),
|
||||||
Eye: (props: any) => React.createElement("svg", { "data-testid": "eye-icon", ...props }),
|
Eye: (props: any) => React.createElement("svg", { "data-testid": "eye-icon", ...props }),
|
||||||
EyeOff: (props: any) => React.createElement("svg", { "data-testid": "eye-off-icon", ...props }),
|
EyeOff: (props: any) => React.createElement("svg", { "data-testid": "eye-off-icon", ...props }),
|
||||||
// FNXC:Test 2026-07-05-11:20: FN-7579 added "ask-user"/"exit-gate" workflow node types to
|
// FNXC:Test 2026-07-05-11:20: FN-7579 added "ask-user"/"exit-gate" workflow node types to
|
||||||
|
|||||||
Reference in New Issue
Block a user