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", () => {