From 4694b4a8c93b4b1af0f53029b2467173521ce2d8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 1 Jul 2026 11:18:57 -0700 Subject: [PATCH] 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) --- .../fn-7387-context-menu-action-order.md | 7 +++++ .../app/components/TaskContextMenu.tsx | 24 ++++++++++------- .../__tests__/TaskContextMenu.test.tsx | 27 +++++++++++-------- 3 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 .changeset/fn-7387-context-menu-action-order.md diff --git a/.changeset/fn-7387-context-menu-action-order.md b/.changeset/fn-7387-context-menu-action-order.md new file mode 100644 index 0000000000..37e89f22a7 --- /dev/null +++ b/.changeset/fn-7387-context-menu-action-order.md @@ -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. diff --git a/packages/dashboard/app/components/TaskContextMenu.tsx b/packages/dashboard/app/components/TaskContextMenu.tsx index 7a8af9fab3..12a7845783 100644 --- a/packages/dashboard/app/components/TaskContextMenu.tsx +++ b/packages/dashboard/app/components/TaskContextMenu.tsx @@ -208,14 +208,8 @@ export function buildTaskActionMenuModel(options: BuildTaskActionMenuModelOption hasAssignedAgent = Boolean(task.assignedAgentId), } = options; const isTaskPaused = Boolean(task.paused || task.userPaused); - const actions: TaskMenuActionDescriptor[] = [ - { - id: "delete", - label: t("taskDetail.delete.btn", "Delete"), - tone: "danger", - onSelect: options.onDelete, - }, - ]; + const actions: TaskMenuActionDescriptor[] = []; + const destructiveActions: TaskMenuActionDescriptor[] = []; if (hasDuplicateHandler) { 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)) { - 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)) { @@ -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 }); } + 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 { actions, moveTransitions: getTaskMoveTransitions(task, t, columnLabel, workflowMoveColumns), diff --git a/packages/dashboard/app/components/__tests__/TaskContextMenu.test.tsx b/packages/dashboard/app/components/__tests__/TaskContextMenu.test.tsx index 358dc9d784..137cae6d21 100644 --- a/packages/dashboard/app/components/__tests__/TaskContextMenu.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskContextMenu.test.tsx @@ -29,23 +29,23 @@ function actionIds(task: Task, overrides: Partial { 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(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(actionIds(makeTask({ column: "in-review" }), { hasDuplicateHandler: true, hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual([ - "delete", "duplicate", "refine", "respecify", - "reset", "pause", + "reset", + "delete", ]); - expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual(["delete", "refine", "respecify"]); - expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true })).toEqual(["delete", "respecify"]); - expect(actionIds(makeTask({ column: "archived" }), { hasResetHandler: true })).toEqual(["delete", "respecify"]); + expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual(["refine", "respecify", "delete"]); + expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true })).toEqual(["respecify", "delete"]); + expect(actionIds(makeTask({ column: "archived" }), { hasResetHandler: true })).toEqual(["respecify", "delete"]); }); 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 }); 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(enabled.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", () => { 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"); const paused = buildTaskActionMenuModel({ @@ -140,8 +142,9 @@ describe("TaskContextMenu shared task action model", () => { ["intake", "Move to Intake"], ["qa", "Move to QA"], ]); - expect(buildModel.actions.map((action) => action.id)).toContain("reset"); - expect(buildModel.actions.map((action) => action.id)).toContain("pause"); + expect(buildModel.actions.map((action) => action.id)).toEqual(["respecify", "pause", "reset", "delete"]); + expect(buildModel.actions.at(-2)?.id).toBe("reset"); + expect(buildModel.actions.at(-1)?.id).toBe("delete"); const completeModel = buildTaskActionMenuModel({ task: makeTask({ column: "complete" }), @@ -152,7 +155,8 @@ describe("TaskContextMenu shared task action model", () => { hasResetHandler: true, 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"]); const archivedModel = buildTaskActionMenuModel({ @@ -163,7 +167,8 @@ describe("TaskContextMenu shared task action model", () => { workflowMoveColumns, 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", () => {