FN-9143: inherit selected workflow in New Task dialog
Make every New Task entry point seed the dialog from the active Board or List workflow. - Resolve implicit workflow selection through the modal manager while preserving explicit choices and All workflows behavior. - Forward selected workflow IDs from List view and navigation-backed dialog entry points. - Add cross-surface regression coverage, operator documentation, and a patch changeset. Files changed: .changeset/fn-9143-new-task-selected-workflow.md | 7 +++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/App.tsx | 4 +- packages/dashboard/app/components/AppModals.tsx | 4 +- packages/dashboard/app/components/Column.tsx | 4 +- .../dashboard/app/components/LeftSidebarNav.tsx | 4 +- packages/dashboard/app/components/ListView.tsx | 7 ++- .../components/__tests__/LeftSidebarNav.test.tsx | 1 + .../app/components/__tests__/ListView.test.tsx | 13 +++-- .../new-task-dialog-selected-workflow.test.tsx | 46 ++++++++++++++++++ .../dashboard/app/components/dashboard/types.ts | 2 +- .../app/hooks/__tests__/useModalManager.test.ts | 55 ++++++++++++++++++++++ packages/dashboard/app/hooks/useModalManager.ts | 19 ++++++-- 13 files changed, 148 insertions(+), 20 deletions(-) Fusion-Task-Id: FN-9143 Fusion-Task-Lineage: 43951c5d-24d2-4243-826a-c6f416607882 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-9143-new-task-selected-workflow.md
Normal file
7
.changeset/fn-9143-new-task-selected-workflow.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: New Task now inherits the workflow selected in Board or List.
|
||||||
|
category: fix
|
||||||
|
dev: Routes New Task opens through the useModalManager.openNewTask inheritance seam.
|
||||||
@@ -482,7 +482,7 @@ Behavior:
|
|||||||
Workflows define how a task moves through planning, execution, review, workflow steps, merge, and any custom graph policy. Most coding tasks can stay on the default Coding workflow, but task and board workflow controls can select a different built-in or custom workflow per task. For the built-in catalog and runtime semantics, see [Workflow Steps → Workflow overview](./workflow-steps.md#workflow-overview).
|
Workflows define how a task moves through planning, execution, review, workflow steps, merge, and any custom graph policy. Most coding tasks can stay on the default Coding workflow, but task and board workflow controls can select a different built-in or custom workflow per task. For the built-in catalog and runtime semantics, see [Workflow Steps → Workflow overview](./workflow-steps.md#workflow-overview).
|
||||||
|
|
||||||
<!-- FNXC:NewTaskWorkflowDropdown 2026-06-30-18:52: The full New Task dialog workflow picker now matches the icon-rich workflow identity used in Board/List selectors while preserving create-time workflowId semantics. -->
|
<!-- FNXC:NewTaskWorkflowDropdown 2026-06-30-18:52: The full New Task dialog workflow picker now matches the icon-rich workflow identity used in Board/List selectors while preserving create-time workflowId semantics. -->
|
||||||
When creating a task from the full **New Task** dialog, the **Workflow** advanced control opens a styled dropdown instead of a native select. Built-in workflows show the Fusion mark, custom workflows show their configured compact icon when present, **No workflow** remains the explicit opt-out, and leaving the picker untouched still inherits the project/default workflow.
|
When creating a task from the full **New Task** dialog, the **Workflow** advanced control opens a styled dropdown instead of a native select. Built-in workflows show the Fusion mark, custom workflows show their configured compact icon when present, **No workflow** remains the explicit opt-out, and leaving the picker untouched still inherits the project/default workflow. Opening the dialog while viewing a specific Board or List workflow preselects that workflow across lane, sidebar, keyboard-shortcut, and description-seeded entry points; opening from **All workflows** instead leaves the picker unset so the project default applies.
|
||||||
|
|
||||||
<!-- FNXC:WorkflowOptionalSteps 2026-07-10-08:00: Operators can now toggle optional workflow steps from the task Edit form as well as the Workflow tab. Edit mode must load the task's resolved workflow catalog without re-seeding from defaultOn, preserving the task's persisted enabledWorkflowSteps until the operator explicitly toggles a step. -->
|
<!-- FNXC:WorkflowOptionalSteps 2026-07-10-08:00: Operators can now toggle optional workflow steps from the task Edit form as well as the Workflow tab. Edit mode must load the task's resolved workflow catalog without re-seeding from defaultOn, preserving the task's persisted enabledWorkflowSteps until the operator explicitly toggles a step. -->
|
||||||
Optional workflow steps can be toggled from the task **Edit** form's **More options → Workflow Steps** control or from the task's **Workflow** tab. The edit form uses the task's resolved workflow and preserves the task's current stored selection when it opens; workflow-authored `defaultOn` values remain a create-time/runtime default, not an edit-form re-seed.
|
Optional workflow steps can be toggled from the task **Edit** form's **More options → Workflow Steps** control or from the task's **Workflow** tab. The edit form uses the task's resolved workflow and preserves the task's current stored selection when it opens; workflow-authored `defaultOn` values remain a create-time/runtime default, not an edit-form re-seed.
|
||||||
|
|||||||
@@ -1249,8 +1249,8 @@ function AppInner() {
|
|||||||
handleTaskViewChange("command-center");
|
handleTaskViewChange("command-center");
|
||||||
}, [handleTaskViewChange]);
|
}, [handleTaskViewChange]);
|
||||||
|
|
||||||
const openNewTaskWithNav = useCallback(() => {
|
const openNewTaskWithNav = useCallback((workflowId?: string | null) => {
|
||||||
modalManager.openNewTask();
|
modalManager.openNewTask(workflowId);
|
||||||
pushNav({ type: "modal", close: modalManager.closeNewTask });
|
pushNav({ type: "modal", close: modalManager.closeNewTask });
|
||||||
}, [modalManager, pushNav]);
|
}, [modalManager, pushNav]);
|
||||||
|
|
||||||
|
|||||||
@@ -232,8 +232,8 @@ export function AppModals({
|
|||||||
modalManager.closeSetupWizard();
|
modalManager.closeSetupWizard();
|
||||||
}, [modalManager.closeSetupWizard, removeNav]);
|
}, [modalManager.closeSetupWizard, removeNav]);
|
||||||
|
|
||||||
const handleOpenNewTask = useCallback(() => {
|
const handleOpenNewTask = useCallback((workflowId?: string | null) => {
|
||||||
modalManager.openNewTask();
|
modalManager.openNewTask(workflowId);
|
||||||
}, [modalManager]);
|
}, [modalManager]);
|
||||||
|
|
||||||
const handleOpenGitHubImport = useCallback(() => {
|
const handleOpenGitHubImport = useCallback(() => {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ interface ColumnProps {
|
|||||||
onOpenGroupModal?: (groupId: string) => void;
|
onOpenGroupModal?: (groupId: string) => void;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
||||||
onNewTask?: () => void;
|
onNewTask?: (workflowId?: string | null) => void;
|
||||||
autoMerge?: boolean;
|
autoMerge?: boolean;
|
||||||
/** Project merge strategy for Task Detail-equivalent card context actions. */
|
/** Project merge strategy for Task Detail-equivalent card context actions. */
|
||||||
mergeStrategy?: string;
|
mergeStrategy?: string;
|
||||||
@@ -924,7 +924,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{onNewTask && (
|
{onNewTask && (
|
||||||
<button className="btn btn-task-create btn-sm" onClick={onNewTask}>
|
<button className="btn btn-task-create btn-sm" onClick={() => onNewTask()}>
|
||||||
+ {t("column.newTask", "New Task")}
|
+ {t("column.newTask", "New Task")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ function persistCollapsed(collapsed: boolean): void {
|
|||||||
export interface LeftSidebarNavProps {
|
export interface LeftSidebarNavProps {
|
||||||
view: TaskView;
|
view: TaskView;
|
||||||
onChangeView: (view: TaskView) => void;
|
onChangeView: (view: TaskView) => void;
|
||||||
onNewTask?: () => void;
|
onNewTask?: (workflowId?: string | null) => void;
|
||||||
onOpenSettings?: () => void;
|
onOpenSettings?: () => void;
|
||||||
mailboxUnreadCount?: number;
|
mailboxUnreadCount?: number;
|
||||||
mailboxPendingApprovalCount?: number;
|
mailboxPendingApprovalCount?: number;
|
||||||
@@ -504,7 +504,7 @@ export function LeftSidebarNav({
|
|||||||
aria-label={newTaskLabel}
|
aria-label={newTaskLabel}
|
||||||
title={newTaskLabel}
|
title={newTaskLabel}
|
||||||
data-testid="sidebar-nav-new-task"
|
data-testid="sidebar-nav-new-task"
|
||||||
onClick={onNewTask}
|
onClick={() => onNewTask()}
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
<span className="left-sidebar-nav__label">{newTaskLabel}</span>
|
<span className="left-sidebar-nav__label">{newTaskLabel}</span>
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ interface ListViewProps {
|
|||||||
openMobileTasksInPopup?: boolean;
|
openMobileTasksInPopup?: boolean;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
globalPaused?: boolean;
|
globalPaused?: boolean;
|
||||||
onNewTask?: () => void;
|
onNewTask?: (workflowId?: string | null) => void;
|
||||||
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
||||||
availableModels?: ModelInfo[];
|
availableModels?: ModelInfo[];
|
||||||
favoriteProviders?: string[];
|
favoriteProviders?: string[];
|
||||||
@@ -2814,7 +2814,10 @@ export function ListView({
|
|||||||
{t("listView.viewOptions", "View")}
|
{t("listView.viewOptions", "View")}
|
||||||
</button>
|
</button>
|
||||||
{onNewTask ? (
|
{onNewTask ? (
|
||||||
<button className="btn btn-task-create btn-sm list-new-task-action" onClick={onNewTask}>
|
<button
|
||||||
|
className="btn btn-task-create btn-sm list-new-task-action"
|
||||||
|
onClick={() => onNewTask(isAllWorkflowsSelected ? undefined : selectedWorkflow?.id)}
|
||||||
|
>
|
||||||
{t("listView.newTask", "+ New Task")}
|
{t("listView.newTask", "+ New Task")}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ describe("LeftSidebarNav", () => {
|
|||||||
|
|
||||||
fireEvent.click(newTaskButton);
|
fireEvent.click(newTaskButton);
|
||||||
expect(onNewTask).toHaveBeenCalledOnce();
|
expect(onNewTask).toHaveBeenCalledOnce();
|
||||||
|
expect(onNewTask).toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits the New Task CTA when no trigger prop is provided", () => {
|
it("omits the New Task CTA when no trigger prop is provided", () => {
|
||||||
|
|||||||
@@ -1914,6 +1914,7 @@ describe("ListView", () => {
|
|||||||
|
|
||||||
it("shows all workflows in ListView without submitting the aggregate sentinel", async () => {
|
it("shows all workflows in ListView without submitting the aggregate sentinel", async () => {
|
||||||
const mockOnQuickCreate = vi.fn().mockResolvedValue({ id: "FN-new" });
|
const mockOnQuickCreate = vi.fn().mockResolvedValue({ id: "FN-new" });
|
||||||
|
const mockOnNewTask = vi.fn();
|
||||||
vi.mocked(fetchBoardWorkflows).mockResolvedValue({
|
vi.mocked(fetchBoardWorkflows).mockResolvedValue({
|
||||||
flagEnabled: true,
|
flagEnabled: true,
|
||||||
defaultWorkflowId: "builtin:coding",
|
defaultWorkflowId: "builtin:coding",
|
||||||
@@ -1931,6 +1932,7 @@ describe("ListView", () => {
|
|||||||
createMockTask({ id: "FN-003", column: "triage", title: "Stale workflow task" }),
|
createMockTask({ id: "FN-003", column: "triage", title: "Stale workflow task" }),
|
||||||
],
|
],
|
||||||
onQuickCreate: mockOnQuickCreate,
|
onQuickCreate: mockOnQuickCreate,
|
||||||
|
onNewTask: mockOnNewTask,
|
||||||
});
|
});
|
||||||
|
|
||||||
await selectWorkflow(ALL_WORKFLOWS_BOARD_VIEW_ID);
|
await selectWorkflow(ALL_WORKFLOWS_BOARD_VIEW_ID);
|
||||||
@@ -1941,6 +1943,9 @@ describe("ListView", () => {
|
|||||||
expect(screen.getByTestId("workflow-switcher")).toHaveTextContent("All workflows");
|
expect(screen.getByTestId("workflow-switcher")).toHaveTextContent("All workflows");
|
||||||
expect(screen.queryByTestId(`workflow-switcher-edit-${ALL_WORKFLOWS_BOARD_VIEW_ID}`)).toBeNull();
|
expect(screen.queryByTestId(`workflow-switcher-edit-${ALL_WORKFLOWS_BOARD_VIEW_ID}`)).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("+ New Task"));
|
||||||
|
expect(mockOnNewTask).toHaveBeenCalledWith(undefined);
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("quick-entry-input"), { target: { value: "Aggregate quick add" } });
|
fireEvent.change(screen.getByTestId("quick-entry-input"), { target: { value: "Aggregate quick add" } });
|
||||||
fireEvent.keyDown(screen.getByTestId("quick-entry-input"), { key: "Enter" });
|
fireEvent.keyDown(screen.getByTestId("quick-entry-input"), { key: "Enter" });
|
||||||
|
|
||||||
@@ -3204,17 +3209,17 @@ describe("ListView", () => {
|
|||||||
expect(screen.getAllByRole("row").filter((r) => r.getAttribute("data-id"))).toHaveLength(1);
|
expect(screen.getAllByRole("row").filter((r) => r.getAttribute("data-id"))).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onNewTask when + New Task button is clicked", () => {
|
it("forwards the selected workflow rather than a click event when + New Task is clicked", () => {
|
||||||
const mockOnNewTask = vi.fn();
|
const mockOnNewTask = vi.fn();
|
||||||
|
|
||||||
renderListView({ onNewTask: mockOnNewTask });
|
renderListView({ onNewTask: mockOnNewTask });
|
||||||
|
|
||||||
const newTaskButton = screen.getByText("+ New Task");
|
fireEvent.click(screen.getByText("+ New Task"));
|
||||||
fireEvent.click(newTaskButton);
|
|
||||||
|
|
||||||
expect(mockOnNewTask).toHaveBeenCalled();
|
expect(mockOnNewTask).toHaveBeenCalledWith("builtin:coding");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("keeps Bulk Edit, View, and + New Task together in the desktop sidebar controls", () => {
|
it("keeps Bulk Edit, View, and + New Task together in the desktop sidebar controls", () => {
|
||||||
renderListView({}, { openViewOptions: false });
|
renderListView({}, { openViewOptions: false });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it, beforeEach } from "vitest";
|
||||||
|
import { act, renderHook } from "@testing-library/react";
|
||||||
|
import { useModalManager } from "../../hooks/useModalManager";
|
||||||
|
import {
|
||||||
|
ALL_WORKFLOWS_BOARD_VIEW_ID,
|
||||||
|
BOARD_WORKFLOW_SELECTION_STORAGE_KEY,
|
||||||
|
} from "../../utils/boardWorkflowSelection";
|
||||||
|
import { scopedKey } from "../../utils/projectStorage";
|
||||||
|
|
||||||
|
const projectId = "new-task-workflow-project";
|
||||||
|
const selectedWorkflowId = "workflow-ideas";
|
||||||
|
const selectionKey = scopedKey(BOARD_WORKFLOW_SELECTION_STORAGE_KEY, projectId);
|
||||||
|
|
||||||
|
describe("New Task dialog selected-workflow inheritance", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the selected workflow across board, list, sidebar, shortcut, and description opens", () => {
|
||||||
|
localStorage.setItem(selectionKey, selectedWorkflowId);
|
||||||
|
const { result } = renderHook(() => useModalManager({ projectId, planningSessions: [] }));
|
||||||
|
|
||||||
|
const surfaces: Array<[string, () => void]> = [
|
||||||
|
["board lane", () => result.current.openNewTask(selectedWorkflowId)],
|
||||||
|
["list", () => result.current.openNewTask(selectedWorkflowId)],
|
||||||
|
["sidebar CTA", () => result.current.openNewTask()],
|
||||||
|
["keyboard shortcut", () => result.current.openNewTask()],
|
||||||
|
["description-seeded", () => result.current.openNewTaskWithDescription("Create from selection")],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [, open] of surfaces) {
|
||||||
|
act(open);
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBe(selectedWorkflowId);
|
||||||
|
act(() => result.current.closeNewTask());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not forward the aggregate board sentinel into the dialog", () => {
|
||||||
|
localStorage.setItem(selectionKey, ALL_WORKFLOWS_BOARD_VIEW_ID);
|
||||||
|
const { result } = renderHook(() => useModalManager({ projectId, planningSessions: [] }));
|
||||||
|
|
||||||
|
act(() => result.current.openNewTask());
|
||||||
|
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -190,7 +190,7 @@ export interface MainContentProps {
|
|||||||
openTaskDetailInMainPanel: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => void;
|
openTaskDetailInMainPanel: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => void;
|
||||||
openGroupModalWithNav: (groupId: string) => void;
|
openGroupModalWithNav: (groupId: string) => void;
|
||||||
handleBoardQuickCreate: (input: TaskCreateInput) => Promise<Task>;
|
handleBoardQuickCreate: (input: TaskCreateInput) => Promise<Task>;
|
||||||
openNewTaskWithNav: () => void;
|
openNewTaskWithNav: (workflowId?: string | null) => void;
|
||||||
subtaskBreakdownEnabled: boolean;
|
subtaskBreakdownEnabled: boolean;
|
||||||
openSubtaskBreakdownWithNav: (description: string, workflowId?: string | null) => void;
|
openSubtaskBreakdownWithNav: (description: string, workflowId?: string | null) => void;
|
||||||
toggleAutoMerge: () => Promise<void>;
|
toggleAutoMerge: () => Promise<void>;
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { act, renderHook, waitFor } from "@testing-library/react";
|
|||||||
import type { Task, TaskDetail } from "@fusion/core";
|
import type { Task, TaskDetail } from "@fusion/core";
|
||||||
import { useModalManager } from "../useModalManager";
|
import { useModalManager } from "../useModalManager";
|
||||||
import { scopedKey } from "../../utils/projectStorage";
|
import { scopedKey } from "../../utils/projectStorage";
|
||||||
|
import {
|
||||||
|
ALL_WORKFLOWS_BOARD_VIEW_ID,
|
||||||
|
BOARD_WORKFLOW_SELECTION_STORAGE_KEY,
|
||||||
|
} from "../../utils/boardWorkflowSelection";
|
||||||
|
|
||||||
function createTaskDetail(id: string): TaskDetail {
|
function createTaskDetail(id: string): TaskDetail {
|
||||||
return {
|
return {
|
||||||
@@ -129,6 +133,57 @@ describe("useModalManager", () => {
|
|||||||
expect(result.current.settingsOpen).toBe(true);
|
expect(result.current.settingsOpen).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("inherits the selected board workflow for new task opens while preserving explicit choices", () => {
|
||||||
|
const projectId = "proj_1";
|
||||||
|
const selectionKey = scopedKey(BOARD_WORKFLOW_SELECTION_STORAGE_KEY, projectId);
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useModalManager({ projectId, planningSessions: [] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.openNewTask();
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBeUndefined();
|
||||||
|
|
||||||
|
localStorage.setItem(selectionKey, "coding");
|
||||||
|
act(() => {
|
||||||
|
result.current.openNewTask();
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBe("coding");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.openNewTask("explicit");
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBe("explicit");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.openNewTask(null);
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBeNull();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.openNewTask({ type: "click" } as never);
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBe("coding");
|
||||||
|
|
||||||
|
localStorage.setItem(selectionKey, ALL_WORKFLOWS_BOARD_VIEW_ID);
|
||||||
|
act(() => {
|
||||||
|
result.current.openNewTask();
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBeUndefined();
|
||||||
|
|
||||||
|
localStorage.setItem(selectionKey, "coding");
|
||||||
|
act(() => {
|
||||||
|
result.current.openNewTaskWithDescription("draft");
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBe("coding");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.closeNewTask();
|
||||||
|
});
|
||||||
|
expect(result.current.newTaskInitialWorkflowId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("opens the new task modal with a seeded description and resets it on close", () => {
|
it("opens the new task modal with a seeded description and resets it on close", () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useModalManager({ projectId: "proj_1", planningSessions: [] }),
|
useModalManager({ projectId: "proj_1", planningSessions: [] }),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Task, TaskDetail } from "@fusion/core";
|
|||||||
import type { SectionId } from "../components/SettingsModal";
|
import type { SectionId } from "../components/SettingsModal";
|
||||||
import type { ToastType } from "./useToast";
|
import type { ToastType } from "./useToast";
|
||||||
import { removeScopedItem } from "../utils/projectStorage";
|
import { removeScopedItem } from "../utils/projectStorage";
|
||||||
|
import { readBoardWorkflowSelection } from "../utils/boardWorkflowSelection";
|
||||||
import { applyLocalTaskPatch } from "./useTasks";
|
import { applyLocalTaskPatch } from "./useTasks";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -292,16 +293,26 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
|||||||
modelOnboardingOpen,
|
modelOnboardingOpen,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TaskWorkflowSelection 2026-08-18-03:33:
|
||||||
|
The New Task dialog must inherit the currently selected board workflow on every open surface.
|
||||||
|
Explicit workflow ids and explicit null take precedence, while the aggregate sentinel never crosses
|
||||||
|
this task-creation boundary.
|
||||||
|
*/
|
||||||
|
const resolveNewTaskInitialWorkflowId = useCallback((workflowId: unknown): string | null | undefined => {
|
||||||
|
if (typeof workflowId === "string" || workflowId === null) return workflowId;
|
||||||
|
return readBoardWorkflowSelection(options.projectId) ?? undefined;
|
||||||
|
}, [options.projectId]);
|
||||||
const openNewTask = useCallback((workflowId?: string | null) => {
|
const openNewTask = useCallback((workflowId?: string | null) => {
|
||||||
setNewTaskInitialDescription(null);
|
setNewTaskInitialDescription(null);
|
||||||
setNewTaskInitialWorkflowId(workflowId);
|
setNewTaskInitialWorkflowId(resolveNewTaskInitialWorkflowId(workflowId));
|
||||||
setNewTaskModalOpen(true);
|
setNewTaskModalOpen(true);
|
||||||
}, []);
|
}, [resolveNewTaskInitialWorkflowId]);
|
||||||
const openNewTaskWithDescription = useCallback((description: string) => {
|
const openNewTaskWithDescription = useCallback((description: string) => {
|
||||||
setNewTaskInitialDescription(description);
|
setNewTaskInitialDescription(description);
|
||||||
setNewTaskInitialWorkflowId(undefined);
|
setNewTaskInitialWorkflowId(resolveNewTaskInitialWorkflowId(undefined));
|
||||||
setNewTaskModalOpen(true);
|
setNewTaskModalOpen(true);
|
||||||
}, []);
|
}, [resolveNewTaskInitialWorkflowId]);
|
||||||
const closeNewTask = useCallback(() => {
|
const closeNewTask = useCallback(() => {
|
||||||
setNewTaskModalOpen(false);
|
setNewTaskModalOpen(false);
|
||||||
setNewTaskInitialDescription(null);
|
setNewTaskInitialDescription(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user