FN-7387: move destructive task actions to menu bottom

Task context menus now place destructive actions after safer task operations.

- Reorder shared task action descriptors so Reset and Delete render at the bottom, with Delete last.
- Update context-menu model coverage for lifecycle, GitHub tracking, pause, and workflow column states.
- Add a patch changeset for the published Fusion CLI package.

Files changed:
 .changeset/fn-7387-context-menu-action-order.md    |  7 ++++++
 .../dashboard/app/components/TaskContextMenu.tsx   | 24 +++++++++++--------
 .../components/__tests__/TaskContextMenu.test.tsx  | 27 +++++++++++++---------
 3 files changed, 38 insertions(+), 20 deletions(-)

Fusion-Task-Id: FN-7387

Fusion-Task-Lineage: f320ed96-d5e7-48f4-9151-59357ae12de7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-01 11:18:57 -07:00
parent 869974cd5a
commit 4694b4a8c9
3 changed files with 38 additions and 20 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Move destructive task context-menu actions to the bottom.
category: fix
dev: Reorders shared TaskContextMenu descriptors so Reset precedes Delete at the end across Board, List, and Detail menus.

View File

@@ -208,14 +208,8 @@ export function buildTaskActionMenuModel(options: BuildTaskActionMenuModelOption
hasAssignedAgent = Boolean(task.assignedAgentId), hasAssignedAgent = Boolean(task.assignedAgentId),
} = options; } = options;
const isTaskPaused = Boolean(task.paused || task.userPaused); const isTaskPaused = Boolean(task.paused || task.userPaused);
const actions: TaskMenuActionDescriptor[] = [ const actions: TaskMenuActionDescriptor[] = [];
{ const destructiveActions: TaskMenuActionDescriptor[] = [];
id: "delete",
label: t("taskDetail.delete.btn", "Delete"),
tone: "danger",
onSelect: options.onDelete,
},
];
if (hasDuplicateHandler) { if (hasDuplicateHandler) {
actions.push({ id: "duplicate", label: t("taskDetail.duplicate.btn", "Duplicate"), onSelect: options.onDuplicate }); actions.push({ id: "duplicate", label: t("taskDetail.duplicate.btn", "Duplicate"), onSelect: options.onDuplicate });
@@ -244,7 +238,7 @@ export function buildTaskActionMenuModel(options: BuildTaskActionMenuModelOption
} }
if (hasResetHandler && isMutableLiveColumn(task.column, currentColumnFlags)) { if (hasResetHandler && isMutableLiveColumn(task.column, currentColumnFlags)) {
actions.push({ id: "reset", label: t("taskDetail.reset.btn", "Reset"), tone: "danger", onSelect: options.onReset }); destructiveActions.push({ id: "reset", label: t("taskDetail.reset.btn", "Reset"), tone: "danger", onSelect: options.onReset });
} }
if (isMutableLiveColumn(task.column, currentColumnFlags)) { if (isMutableLiveColumn(task.column, currentColumnFlags)) {
@@ -259,6 +253,18 @@ export function buildTaskActionMenuModel(options: BuildTaskActionMenuModelOption
actions.push({ id: "paused-by-agent", label: t("taskDetail.pause.pausedByAgent", "Paused by agent"), tone: "note", disabled: true }); actions.push({ id: "paused-by-agent", label: t("taskDetail.pause.pausedByAgent", "Paused by agent"), tone: "note", disabled: true });
} }
destructiveActions.push({
id: "delete",
label: t("taskDetail.delete.btn", "Delete"),
tone: "danger",
onSelect: options.onDelete,
});
/*
FNXC:TaskContextMenu 2026-07-01-00:00:
Popup context menus intentionally group destructive Reset and Delete actions at the bottom, with Delete last, so Board, List, and Detail hosts share the safer operator action order without forking availability or confirmation behavior.
*/
actions.push(...destructiveActions);
return { return {
actions, actions,
moveTransitions: getTaskMoveTransitions(task, t, columnLabel, workflowMoveColumns), moveTransitions: getTaskMoveTransitions(task, t, columnLabel, workflowMoveColumns),

View File

@@ -29,23 +29,23 @@ function actionIds(task: Task, overrides: Partial<Parameters<typeof buildTaskAct
describe("TaskContextMenu shared task action model", () => { describe("TaskContextMenu shared task action model", () => {
it("mirrors detail Actions menu availability across lifecycle states", () => { it("mirrors detail Actions menu availability across lifecycle states", () => {
expect(actionIds(makeTask({ column: "triage" }))).toEqual(["delete", "respecify", "pause"]); expect(actionIds(makeTask({ column: "triage" }))).toEqual(["respecify", "pause", "delete"]);
expect(buildTaskActionMenuModel({ task: makeTask({ column: "triage" }), t, columnLabel: columnLabel as any }).shouldShowActionsMenu).toBe(false); expect(buildTaskActionMenuModel({ task: makeTask({ column: "triage" }), t, columnLabel: columnLabel as any }).shouldShowActionsMenu).toBe(false);
expect(actionIds(makeTask({ column: "triage", status: "failed" as any }), { canRetryTask: true, hasRetryHandler: true })).toContain("retry"); expect(actionIds(makeTask({ column: "triage", status: "failed" as any }), { canRetryTask: true, hasRetryHandler: true })).toEqual(["respecify", "retry", "pause", "delete"]);
expect(buildTaskActionMenuModel({ task: makeTask({ column: "triage", status: "failed" as any }), t, columnLabel: columnLabel as any, canRetryTask: true, hasRetryHandler: true }).shouldShowActionsMenu).toBe(true); expect(buildTaskActionMenuModel({ task: makeTask({ column: "triage", status: "failed" as any }), t, columnLabel: columnLabel as any, canRetryTask: true, hasRetryHandler: true }).shouldShowActionsMenu).toBe(true);
expect(actionIds(makeTask({ column: "in-review" }), { hasDuplicateHandler: true, hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual([ expect(actionIds(makeTask({ column: "in-review" }), { hasDuplicateHandler: true, hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual([
"delete",
"duplicate", "duplicate",
"refine", "refine",
"respecify", "respecify",
"reset",
"pause", "pause",
"reset",
"delete",
]); ]);
expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual(["delete", "refine", "respecify"]); expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual(["refine", "respecify", "delete"]);
expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true })).toEqual(["delete", "respecify"]); expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true })).toEqual(["respecify", "delete"]);
expect(actionIds(makeTask({ column: "archived" }), { hasResetHandler: true })).toEqual(["delete", "respecify"]); expect(actionIds(makeTask({ column: "archived" }), { hasResetHandler: true })).toEqual(["respecify", "delete"]);
}); });
it("exposes GitHub tracking enablement only for untracked tasks with a host callback", () => { it("exposes GitHub tracking enablement only for untracked tasks with a host callback", () => {
@@ -77,6 +77,7 @@ describe("TaskContextMenu shared task action model", () => {
const noCallback = buildTaskActionMenuModel({ task: makeTask(), t, columnLabel: columnLabel as any }); const noCallback = buildTaskActionMenuModel({ task: makeTask(), t, columnLabel: columnLabel as any });
expect(untracked.actions.find((action) => action.id === "enable-github-tracking")?.label).toBe("Enable GitHub tracking"); expect(untracked.actions.find((action) => action.id === "enable-github-tracking")?.label).toBe("Enable GitHub tracking");
expect(untracked.actions.map((action) => action.id)).toEqual(["respecify", "enable-github-tracking", "pause", "delete"]);
expect(disabled.actions.map((action) => action.id)).toContain("enable-github-tracking"); expect(disabled.actions.map((action) => action.id)).toContain("enable-github-tracking");
expect(enabled.actions.map((action) => action.id)).not.toContain("enable-github-tracking"); expect(enabled.actions.map((action) => action.id)).not.toContain("enable-github-tracking");
expect(linked.actions.map((action) => action.id)).not.toContain("enable-github-tracking"); expect(linked.actions.map((action) => action.id)).not.toContain("enable-github-tracking");
@@ -88,6 +89,7 @@ describe("TaskContextMenu shared task action model", () => {
it("exposes pause, unpause, and paused-by-agent note with detail labels", () => { it("exposes pause, unpause, and paused-by-agent note with detail labels", () => {
const active = buildTaskActionMenuModel({ task: makeTask(), t, columnLabel: columnLabel as any }); const active = buildTaskActionMenuModel({ task: makeTask(), t, columnLabel: columnLabel as any });
expect(active.actions.map((action) => action.id)).toEqual(["respecify", "pause", "delete"]);
expect(active.actions.find((action) => action.id === "pause")?.label).toBe("Pause"); expect(active.actions.find((action) => action.id === "pause")?.label).toBe("Pause");
const paused = buildTaskActionMenuModel({ const paused = buildTaskActionMenuModel({
@@ -140,8 +142,9 @@ describe("TaskContextMenu shared task action model", () => {
["intake", "Move to Intake"], ["intake", "Move to Intake"],
["qa", "Move to QA"], ["qa", "Move to QA"],
]); ]);
expect(buildModel.actions.map((action) => action.id)).toContain("reset"); expect(buildModel.actions.map((action) => action.id)).toEqual(["respecify", "pause", "reset", "delete"]);
expect(buildModel.actions.map((action) => action.id)).toContain("pause"); expect(buildModel.actions.at(-2)?.id).toBe("reset");
expect(buildModel.actions.at(-1)?.id).toBe("delete");
const completeModel = buildTaskActionMenuModel({ const completeModel = buildTaskActionMenuModel({
task: makeTask({ column: "complete" }), task: makeTask({ column: "complete" }),
@@ -152,7 +155,8 @@ describe("TaskContextMenu shared task action model", () => {
hasResetHandler: true, hasResetHandler: true,
onOpenRefine: vi.fn(), onOpenRefine: vi.fn(),
}); });
expect(completeModel.actions.map((action) => action.id)).toEqual(["delete", "refine", "respecify"]); expect(completeModel.actions.map((action) => action.id)).toEqual(["refine", "respecify", "delete"]);
expect(completeModel.actions.map((action) => action.id)).not.toContain("reset");
expect(completeModel.moveTransitions.map((action) => action.column)).toEqual(["qa", "cold-storage"]); expect(completeModel.moveTransitions.map((action) => action.column)).toEqual(["qa", "cold-storage"]);
const archivedModel = buildTaskActionMenuModel({ const archivedModel = buildTaskActionMenuModel({
@@ -163,7 +167,8 @@ describe("TaskContextMenu shared task action model", () => {
workflowMoveColumns, workflowMoveColumns,
hasResetHandler: true, hasResetHandler: true,
}); });
expect(archivedModel.actions.map((action) => action.id)).toEqual(["delete", "respecify"]); expect(archivedModel.actions.map((action) => action.id)).toEqual(["respecify", "delete"]);
expect(archivedModel.actions.map((action) => action.id)).not.toContain("reset");
}); });
it("mirrors in-review merge and manual PR status actions", () => { it("mirrors in-review merge and manual PR status actions", () => {