FN-8476: restore Ideas detail move action
Restore Coding (Ideas) task-detail moves to Todo while preserving supplied workflow field definitions. - Resolve selected workflow columns independently for detail move actions - Use workflow-specific primary action labels and add desktop/mobile regression coverage - Add a patch changeset for the restored detail action Files changed: .changeset/fn-8476-coding-ideas-move.md | 7 ++ .../dashboard/app/components/TaskDetailModal.tsx | 85 +++++++++++++------- .../TaskDetailModal.custom-fields.test.tsx | 9 ++- .../__tests__/TaskDetailModal.rendering.test.tsx | 91 ++++++++++++++++++++++ 4 files changed, 160 insertions(+), 32 deletions(-) Fusion-Task-Id: FN-8476 Fusion-Task-Lineage: 37975987-91e4-47f2-9fa4-57b2074391f9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8476-coding-ideas-move.md
Normal file
7
.changeset/fn-8476-coding-ideas-move.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Restore the Coding Ideas detail action to move parked ideas to Todo.
|
||||||
|
category: fix
|
||||||
|
dev: Detail workflow move targets now resolve independently from supplied custom-field definitions.
|
||||||
@@ -78,6 +78,7 @@ import { getRelativeTimeBucket } from "../utils/relativeTimeAgo";
|
|||||||
import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval";
|
import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval";
|
||||||
import { ACTIVE_STATUSES, resolveEffectiveExecutor, resolveEffectivePlanning, resolveEffectiveValidator, type ModelSelection } from "./effective-model-resolution";
|
import { ACTIVE_STATUSES, resolveEffectiveExecutor, resolveEffectivePlanning, resolveEffectiveValidator, type ModelSelection } from "./effective-model-resolution";
|
||||||
import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel } from "./TaskContextMenu";
|
import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel } from "./TaskContextMenu";
|
||||||
|
import type { TaskContextMenuColumnFlags, TaskContextMenuColumnMetadata } from "./TaskContextMenu";
|
||||||
import { FLOATING_WINDOW_GEOMETRY_CHANGE_EVENT } from "./FloatingWindow";
|
import { FLOATING_WINDOW_GEOMETRY_CHANGE_EVENT } from "./FloatingWindow";
|
||||||
import { useFileBrowser } from "../context/FileBrowserContext";
|
import { useFileBrowser } from "../context/FileBrowserContext";
|
||||||
import type { DetailTaskInitialActionRequest } from "../hooks/useModalManager";
|
import type { DetailTaskInitialActionRequest } from "../hooks/useModalManager";
|
||||||
@@ -384,9 +385,9 @@ export interface TaskDetailModalProps {
|
|||||||
/** Project setting: true restores Chat-first tab order/default; false or missing uses Activity-first. */
|
/** Project setting: true restores Chat-first tab order/default; false or missing uses Activity-first. */
|
||||||
taskDetailChatFirst?: boolean;
|
taskDetailChatFirst?: boolean;
|
||||||
/** Pre-resolved workflow field defs for this task's workflow (U13/KTD-14).
|
/** Pre-resolved workflow field defs for this task's workflow (U13/KTD-14).
|
||||||
* When provided (e.g. threaded from a Board that already holds the payload)
|
* When provided, these remain authoritative for custom-field rendering.
|
||||||
* the modal skips its own board-workflows fetch entirely. Falls back to the
|
* Move metadata still resolves independently because field definitions do not
|
||||||
* self-fetch when absent (e.g. modal opened from non-board contexts). */
|
* identify the selected workflow's ordered columns. */
|
||||||
workflowFieldDefs?: WorkflowFieldDefinition[] | null;
|
workflowFieldDefs?: WorkflowFieldDefinition[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,13 +503,27 @@ function normalizeTaskPriorityValue(priority: Task["priority"]): TaskPriority {
|
|||||||
: DEFAULT_TASK_PRIORITY;
|
: DEFAULT_TASK_PRIORITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTaskWorkflowMetadata(payload: BoardWorkflowsPayload, taskId: string): { id: string; name: string; icon?: string; fields: WorkflowFieldDefinition[] | null } | null {
|
interface TaskWorkflowMetadata {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
fields: WorkflowFieldDefinition[] | null;
|
||||||
|
moveColumns: TaskContextMenuColumnMetadata[];
|
||||||
|
currentColumnFlags?: TaskContextMenuColumnFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTaskWorkflowMetadata(payload: BoardWorkflowsPayload, task: Pick<Task, "id" | "column">): TaskWorkflowMetadata | null {
|
||||||
if (payload.flagEnabled !== true) return null;
|
if (payload.flagEnabled !== true) return null;
|
||||||
const workflowId = payload.taskWorkflowIds[taskId] ?? payload.defaultWorkflowId;
|
const workflowId = payload.taskWorkflowIds[task.id] ?? payload.defaultWorkflowId;
|
||||||
const workflow = payload.workflows.find((candidate) => candidate.id === workflowId);
|
const workflow = payload.workflows.find((candidate) => candidate.id === workflowId);
|
||||||
const name = workflow?.name?.trim();
|
const name = workflow?.name?.trim();
|
||||||
if (!workflow || !name) return null;
|
if (!workflow || !name) return null;
|
||||||
return { id: workflow.id, name, icon: workflow.icon, fields: workflow.fields ?? null };
|
|
||||||
|
const moveColumns = workflow.columns
|
||||||
|
.filter((column) => column.flags.hiddenFromBoard !== true)
|
||||||
|
.map((column) => ({ id: column.id as ColumnId, label: column.name, flags: column.flags }));
|
||||||
|
const currentColumnFlags = moveColumns.find((column) => column.id === task.column)?.flags;
|
||||||
|
return { id: workflow.id, name, icon: workflow.icon, fields: workflow.fields ?? null, moveColumns, currentColumnFlags };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeExecutionModeValue(executionMode: Task["executionMode"]): "standard" | "fast" {
|
function normalizeExecutionModeValue(executionMode: Task["executionMode"]): "standard" | "fast" {
|
||||||
@@ -931,13 +946,18 @@ export function TaskDetailContent({
|
|||||||
/*
|
/*
|
||||||
FNXC:WorkflowBadges 2026-06-29-00:00:
|
FNXC:WorkflowBadges 2026-06-29-00:00:
|
||||||
Task details need a stable workflow-name badge because aggregate Board cards can mix tasks from multiple workflows. Resolve the badge name and custom field definitions from the same board-workflows payload so detail headers do not issue duplicate workflow-metadata fetches.
|
Task details need a stable workflow-name badge because aggregate Board cards can mix tasks from multiple workflows. Resolve the badge name and custom field definitions from the same board-workflows payload so detail headers do not issue duplicate workflow-metadata fetches.
|
||||||
|
|
||||||
|
FNXC:CodingIdeasWorkflow 2026-07-21-00:00:
|
||||||
|
Coding (Ideas) intake cards need selected-workflow columns to derive their truthful
|
||||||
|
move target through TaskContextMenu. Callers may supply field definitions, but fields
|
||||||
|
cannot encode ordered columns, so resolve move metadata independently without replacing
|
||||||
|
the caller-owned field definitions.
|
||||||
*/
|
*/
|
||||||
const [taskWorkflowBadge, setTaskWorkflowBadge] = useState<{ id: string; name: string; icon?: string } | null>(null);
|
const [taskWorkflowBadge, setTaskWorkflowBadge] = useState<{ id: string; name: string; icon?: string } | null>(null);
|
||||||
|
const [workflowMoveMetadata, setWorkflowMoveMetadata] = useState<Pick<TaskWorkflowMetadata, "moveColumns" | "currentColumnFlags"> | null>(null);
|
||||||
// Custom field definitions (U13/KTD-14). Resolved for this task's workflow
|
// Custom field definitions (U13/KTD-14). Resolved for this task's workflow
|
||||||
// from the board-workflows payload; absent when the workflow declares none,
|
// from the board-workflows payload; absent when the workflow declares none,
|
||||||
// in which case the fields section renders nothing (today's UI byte-identical).
|
// in which case the fields section renders nothing (today's UI byte-identical).
|
||||||
// When `workflowFieldDefsProp` is provided by the caller (e.g. the Board
|
|
||||||
// already holds the payload) we skip the self-fetch entirely.
|
|
||||||
const [customFieldDefs, setCustomFieldDefs] = useState<WorkflowFieldDefinition[] | null>(
|
const [customFieldDefs, setCustomFieldDefs] = useState<WorkflowFieldDefinition[] | null>(
|
||||||
workflowFieldDefsProp !== undefined ? (workflowFieldDefsProp ?? null) : null,
|
workflowFieldDefsProp !== undefined ? (workflowFieldDefsProp ?? null) : null,
|
||||||
);
|
);
|
||||||
@@ -949,40 +969,45 @@ export function TaskDetailContent({
|
|||||||
setCustomFieldValues(task.customFields ?? {});
|
setCustomFieldValues(task.customFields ?? {});
|
||||||
}, [task.id, task.customFields]);
|
}, [task.id, task.customFields]);
|
||||||
|
|
||||||
// Resolve this task's workflow field definitions and display name once per task. Skipped when
|
// Resolve selected-workflow display and move metadata once per task. A supplied fields prop
|
||||||
// the caller supplies `workflowFieldDefs` directly (Board context). Best-effort:
|
// avoids a duplicate field lookup, but cannot replace this ordered-column lookup.
|
||||||
// a failed fetch (or flag-OFF empty payload) leaves defs/name null → no section/badge.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workflowFieldDefsProp !== undefined) {
|
|
||||||
// Prop-driven path: keep in sync if the prop changes (task switch etc.).
|
|
||||||
setCustomFieldDefs(workflowFieldDefsProp ?? null);
|
|
||||||
setTaskWorkflowBadge(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/*
|
/*
|
||||||
FNXC:WorkflowBadges 2026-06-29-16:48:
|
FNXC:WorkflowBadges 2026-06-29-16:48:
|
||||||
Mounted task-detail hosts can swap from one task to another (List split-pane, right dock, floating windows). Clear the previous workflow badge before the shared board-workflows lookup resolves so aggregate-board context never shows a stale cross-workflow label.
|
Mounted task-detail hosts can swap from one task to another (List split-pane, right dock, floating windows). Clear the previous workflow badge and move metadata before the shared board-workflows lookup resolves so aggregate-board context never shows stale cross-workflow labels or targets.
|
||||||
*/
|
*/
|
||||||
setCustomFieldDefs(null);
|
if (workflowFieldDefsProp !== undefined) {
|
||||||
|
setCustomFieldDefs(workflowFieldDefsProp ?? null);
|
||||||
|
} else {
|
||||||
|
setCustomFieldDefs(null);
|
||||||
|
}
|
||||||
setTaskWorkflowBadge(null);
|
setTaskWorkflowBadge(null);
|
||||||
|
setWorkflowMoveMetadata(null);
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void fetchBoardWorkflows(projectId)
|
void fetchBoardWorkflows(projectId)
|
||||||
.then((payload) => {
|
.then((payload) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const metadata = resolveTaskWorkflowMetadata(payload, task.id);
|
const metadata = resolveTaskWorkflowMetadata(payload, task);
|
||||||
setCustomFieldDefs(metadata?.fields ?? null);
|
if (workflowFieldDefsProp === undefined) {
|
||||||
|
setCustomFieldDefs(metadata?.fields ?? null);
|
||||||
|
}
|
||||||
setTaskWorkflowBadge(metadata ? { id: metadata.id, name: metadata.name, icon: metadata.icon } : null);
|
setTaskWorkflowBadge(metadata ? { id: metadata.id, name: metadata.name, icon: metadata.icon } : null);
|
||||||
|
setWorkflowMoveMetadata(metadata ? {
|
||||||
|
moveColumns: metadata.moveColumns,
|
||||||
|
currentColumnFlags: metadata.currentColumnFlags,
|
||||||
|
} : null);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setCustomFieldDefs(null);
|
if (workflowFieldDefsProp === undefined) setCustomFieldDefs(null);
|
||||||
setTaskWorkflowBadge(null);
|
setTaskWorkflowBadge(null);
|
||||||
|
setWorkflowMoveMetadata(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [task.id, projectId, workflowFieldDefsProp]);
|
}, [task, projectId, workflowFieldDefsProp]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:PlannerOversight 2026-07-04-17:00:
|
FNXC:PlannerOversight 2026-07-04-17:00:
|
||||||
@@ -3516,6 +3541,8 @@ export function TaskDetailContent({
|
|||||||
task,
|
task,
|
||||||
t,
|
t,
|
||||||
columnLabel,
|
columnLabel,
|
||||||
|
currentColumnFlags: workflowMoveMetadata?.currentColumnFlags,
|
||||||
|
workflowMoveColumns: workflowMoveMetadata?.moveColumns,
|
||||||
canRetryTask,
|
canRetryTask,
|
||||||
hasDuplicateHandler: Boolean(onDuplicateTask),
|
hasDuplicateHandler: Boolean(onDuplicateTask),
|
||||||
hasRetryHandler: Boolean(onRetryTask),
|
hasRetryHandler: Boolean(onRetryTask),
|
||||||
@@ -3540,6 +3567,7 @@ export function TaskDetailContent({
|
|||||||
task,
|
task,
|
||||||
t,
|
t,
|
||||||
columnLabel,
|
columnLabel,
|
||||||
|
workflowMoveMetadata,
|
||||||
canRetryTask,
|
canRetryTask,
|
||||||
onDuplicateTask,
|
onDuplicateTask,
|
||||||
onRetryTask,
|
onRetryTask,
|
||||||
@@ -3561,7 +3589,8 @@ export function TaskDetailContent({
|
|||||||
handleCheckPrStatus,
|
handleCheckPrStatus,
|
||||||
handleBypassReview,
|
handleBypassReview,
|
||||||
]);
|
]);
|
||||||
const primaryMoveTransition = taskActionMenuModel.moveTransitions[0]?.column;
|
const primaryMoveAction = taskActionMenuModel.moveTransitions[0];
|
||||||
|
const primaryMoveTransition = primaryMoveAction?.column;
|
||||||
const secondaryMoveTransitions = taskActionMenuModel.moveTransitions.slice(1);
|
const secondaryMoveTransitions = taskActionMenuModel.moveTransitions.slice(1);
|
||||||
const hasSecondaryMoveOptions = secondaryMoveTransitions.length > 0;
|
const hasSecondaryMoveOptions = secondaryMoveTransitions.length > 0;
|
||||||
const reviewAction = taskActionMenuModel.reviewAction;
|
const reviewAction = taskActionMenuModel.reviewAction;
|
||||||
@@ -6334,12 +6363,12 @@ export function TaskDetailContent({
|
|||||||
onClick={handleMoveButtonClick}
|
onClick={handleMoveButtonClick}
|
||||||
onKeyDown={handleMoveButtonKeyDown}
|
onKeyDown={handleMoveButtonKeyDown}
|
||||||
disabled={!primaryMoveTransition}
|
disabled={!primaryMoveTransition}
|
||||||
aria-label={primaryMoveTransition ? t("taskDetail.move.moveTo", "Move to {{column}}", { column: columnLabel(primaryMoveTransition) }) : undefined}
|
aria-label={primaryMoveAction?.primaryLabel}
|
||||||
aria-haspopup={hasSecondaryMoveOptions ? "menu" : undefined}
|
aria-haspopup={hasSecondaryMoveOptions ? "menu" : undefined}
|
||||||
aria-expanded={hasSecondaryMoveOptions ? showMoveMenu : undefined}
|
aria-expanded={hasSecondaryMoveOptions ? showMoveMenu : undefined}
|
||||||
>
|
>
|
||||||
<span className="detail-move-btn__label">
|
<span className="detail-move-btn__label">
|
||||||
{t("taskDetail.move.moveTo", "Move to {{column}}", { column: primaryMoveTransition ? columnLabel(primaryMoveTransition) : "" })}
|
{primaryMoveAction?.primaryLabel ?? t("taskDetail.move.moveTo", "Move to {{column}}", { column: "" })}
|
||||||
</span>
|
</span>
|
||||||
{hasSecondaryMoveOptions && (
|
{hasSecondaryMoveOptions && (
|
||||||
<span className="detail-move-btn__arrow" aria-hidden="true">
|
<span className="detail-move-btn__arrow" aria-hidden="true">
|
||||||
@@ -6381,12 +6410,12 @@ export function TaskDetailContent({
|
|||||||
onClick={handleMoveButtonClick}
|
onClick={handleMoveButtonClick}
|
||||||
onKeyDown={handleMoveButtonKeyDown}
|
onKeyDown={handleMoveButtonKeyDown}
|
||||||
disabled={!primaryMoveTransition}
|
disabled={!primaryMoveTransition}
|
||||||
aria-label={primaryMoveTransition ? t("taskDetail.move.moveTo", "Move to {{column}}", { column: columnLabel(primaryMoveTransition) }) : undefined}
|
aria-label={primaryMoveAction?.primaryLabel}
|
||||||
aria-haspopup={hasSecondaryMoveOptions ? "menu" : undefined}
|
aria-haspopup={hasSecondaryMoveOptions ? "menu" : undefined}
|
||||||
aria-expanded={hasSecondaryMoveOptions ? showMoveMenu : undefined}
|
aria-expanded={hasSecondaryMoveOptions ? showMoveMenu : undefined}
|
||||||
>
|
>
|
||||||
<span className="detail-move-btn__label">
|
<span className="detail-move-btn__label">
|
||||||
{t("taskDetail.move.moveTo", "Move to {{column}}", { column: primaryMoveTransition ? columnLabel(primaryMoveTransition) : "" })}
|
{primaryMoveAction?.primaryLabel ?? t("taskDetail.move.moveTo", "Move to {{column}}", { column: "" })}
|
||||||
</span>
|
</span>
|
||||||
{hasSecondaryMoveOptions && (
|
{hasSecondaryMoveOptions && (
|
||||||
<span className="detail-move-btn__arrow" aria-hidden="true">
|
<span className="detail-move-btn__arrow" aria-hidden="true">
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe("TaskDetailModal custom fields (U13/KTD-14)", () => {
|
|||||||
expect((screen.getByLabelText("Owner") as HTMLInputElement).value).toBe("alice");
|
expect((screen.getByLabelText("Owner") as HTMLInputElement).value).toBe("alice");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses workflowFieldDefs prop directly and skips the board-workflows fetch", async () => {
|
it("uses workflowFieldDefs prop directly while resolving independent move metadata", async () => {
|
||||||
const fetchSpy = vi.spyOn(dashboardApi, "fetchBoardWorkflows");
|
const fetchSpy = vi.spyOn(dashboardApi, "fetchBoardWorkflows");
|
||||||
const defs: WorkflowFieldDefinition[] = [
|
const defs: WorkflowFieldDefinition[] = [
|
||||||
{ id: "owner", name: "Owner", type: "string", render: { placement: "detail" } },
|
{ id: "owner", name: "Owner", type: "string", render: { placement: "detail" } },
|
||||||
@@ -90,8 +90,8 @@ describe("TaskDetailModal custom fields (U13/KTD-14)", () => {
|
|||||||
);
|
);
|
||||||
await waitFor(() => expect(screen.getByTestId("task-fields-section")).toBeTruthy());
|
await waitFor(() => expect(screen.getByTestId("task-fields-section")).toBeTruthy());
|
||||||
expect((screen.getByLabelText("Owner") as HTMLInputElement).value).toBe("bob");
|
expect((screen.getByLabelText("Owner") as HTMLInputElement).value).toBe("bob");
|
||||||
// The fetch must NOT have been triggered since the prop was provided.
|
// Supplied fields remain authoritative; the one fetch resolves move columns independently.
|
||||||
expect(fetchSpy).not.toHaveBeenCalled();
|
await waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders no fields section when workflowFieldDefs prop is an empty array", async () => {
|
it("renders no fields section when workflowFieldDefs prop is an empty array", async () => {
|
||||||
@@ -113,6 +113,7 @@ describe("TaskDetailModal custom fields (U13/KTD-14)", () => {
|
|||||||
// Give React a tick to settle; no section should appear.
|
// Give React a tick to settle; no section should appear.
|
||||||
await new Promise((r) => setTimeout(r, 50));
|
await new Promise((r) => setTimeout(r, 50));
|
||||||
expect(screen.queryByTestId("task-fields-section")).toBeNull();
|
expect(screen.queryByTestId("task-fields-section")).toBeNull();
|
||||||
expect(fetchSpy).not.toHaveBeenCalled();
|
// Empty supplied fields still avoid replacement; the lookup is only for move metadata.
|
||||||
|
await waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(1));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2851,4 +2851,95 @@ describe("TaskDetailModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Coding (Ideas) workflow detail moves", () => {
|
||||||
|
const ideasWorkflowPayload = {
|
||||||
|
flagEnabled: true,
|
||||||
|
defaultWorkflowId: "builtin:coding-ideas",
|
||||||
|
workflows: [{
|
||||||
|
id: "builtin:coding-ideas",
|
||||||
|
name: "Coding (Ideas)",
|
||||||
|
columns: [
|
||||||
|
{ id: "ideas", name: "Ideas", flags: { intake: true } },
|
||||||
|
{ id: "todo", name: "Todo", flags: {} },
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
taskWorkflowIds: { "FN-ideas": "builtin:coding-ideas" },
|
||||||
|
};
|
||||||
|
const ideasTask = () => makeTask({ id: "FN-ideas", column: "ideas", title: "Parked coding idea" });
|
||||||
|
const setViewport = (width: number) => {
|
||||||
|
Object.defineProperty(window, "innerWidth", { configurable: true, value: width });
|
||||||
|
window.dispatchEvent(new Event("resize"));
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(dashboardApi.fetchBoardWorkflows).mockReset();
|
||||||
|
vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValue(ideasWorkflowPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setViewport(1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function expectIdeasMove(renderDetail: () => void, onMoveTask: ReturnType<typeof vi.fn>) {
|
||||||
|
renderDetail();
|
||||||
|
const moveButton = await screen.findByRole("button", { name: "Move to Todo" });
|
||||||
|
expect(moveButton).toBeEnabled();
|
||||||
|
await userEvent.click(moveButton);
|
||||||
|
expect(onMoveTask).toHaveBeenCalledWith("FN-ideas", "todo", undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("moves an Ideas task to Todo from the modal at desktop width", async () => {
|
||||||
|
setViewport(1024);
|
||||||
|
const onMoveTask = vi.fn(async () => ideasTask());
|
||||||
|
await expectIdeasMove(() => {
|
||||||
|
render(<TaskDetailModal initialTab="definition" task={ideasTask()} onClose={noop} onMoveTask={onMoveTask} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />);
|
||||||
|
}, onMoveTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves an Ideas task to Todo from the modal at 375px", async () => {
|
||||||
|
setViewport(375);
|
||||||
|
const onMoveTask = vi.fn(async () => ideasTask());
|
||||||
|
await expectIdeasMove(() => {
|
||||||
|
render(<TaskDetailModal initialTab="definition" task={ideasTask()} onClose={noop} onMoveTask={onMoveTask} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />);
|
||||||
|
}, onMoveTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves an Ideas task to Todo from embedded detail at desktop width", async () => {
|
||||||
|
setViewport(1024);
|
||||||
|
const onMoveTask = vi.fn(async () => ideasTask());
|
||||||
|
await expectIdeasMove(() => {
|
||||||
|
render(<TaskDetailContent embedded initialTab="definition" task={ideasTask()} onMoveTask={onMoveTask} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />);
|
||||||
|
}, onMoveTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves an Ideas task to Todo from embedded detail at 375px", async () => {
|
||||||
|
setViewport(375);
|
||||||
|
const onMoveTask = vi.fn(async () => ideasTask());
|
||||||
|
await expectIdeasMove(() => {
|
||||||
|
render(<TaskDetailContent embedded initialTab="definition" task={ideasTask()} onMoveTask={onMoveTask} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />);
|
||||||
|
}, onMoveTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps supplied fields while independently resolving and clearing Ideas move metadata", async () => {
|
||||||
|
const onMoveTask = vi.fn(async () => ideasTask());
|
||||||
|
const workflowFieldDefs = [{ id: "owner", name: "Owner", type: "string" as const, render: { placement: "detail" as const } }];
|
||||||
|
const { rerender } = render(
|
||||||
|
<TaskDetailModal initialTab="definition" task={ideasTask()} workflowFieldDefs={workflowFieldDefs} onClose={noop} onMoveTask={onMoveTask} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByLabelText("Owner")).toBeInTheDocument();
|
||||||
|
const moveButton = await screen.findByRole("button", { name: "Move to Todo" });
|
||||||
|
await userEvent.click(moveButton);
|
||||||
|
expect(onMoveTask).toHaveBeenCalledWith("FN-ideas", "todo", undefined);
|
||||||
|
expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce({ flagEnabled: false, defaultWorkflowId: "", workflows: [], taskWorkflowIds: {} });
|
||||||
|
rerender(
|
||||||
|
<TaskDetailModal initialTab="definition" task={makeTask({ id: "FN-unresolved", column: "ideas" })} workflowFieldDefs={workflowFieldDefs} onClose={noop} onMoveTask={onMoveTask} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(2));
|
||||||
|
expect(screen.queryByRole("button", { name: "Move to Todo" })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user