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:
7
.changeset/fn-7387-context-menu-action-order.md
Normal file
7
.changeset/fn-7387-context-menu-action-order.md
Normal 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.
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user