feat(FN-1940): add onboarding first-task success handoff
- Track the first task created from onboarding in AppModals and pass it back into the onboarding modal - Keep onboarding open after launching New Task, then switch to a task-created success state when a task is returned - Add success actions to view the created task or return to the dashboard, while preserving the no-task completion path - Style the task-created success panel and add comprehensive ModelOnboardingModal tests for transition, rendering, and button behavior
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import type { ProjectInfo } from "../api";
|
import type { ProjectInfo } from "../api";
|
||||||
import type { ColorTheme, Column, MergeResult, Task, TaskCreateInput, ThemeMode } from "@fusion/core";
|
import type { ColorTheme, Column, MergeResult, Task, TaskCreateInput, ThemeMode } from "@fusion/core";
|
||||||
import type { UseProjectActionsResult } from "../hooks/useProjectActions";
|
import type { UseProjectActionsResult } from "../hooks/useProjectActions";
|
||||||
@@ -75,9 +76,42 @@ export function AppModals({
|
|||||||
onSettingsClose,
|
onSettingsClose,
|
||||||
onReopenOnboarding,
|
onReopenOnboarding,
|
||||||
}: AppModalsProps) {
|
}: AppModalsProps) {
|
||||||
|
const [firstCreatedTask, setFirstCreatedTask] = useState<Task | null>(null);
|
||||||
|
|
||||||
// Use the override handler if provided, otherwise fall back to modalManager.closeSettings
|
// Use the override handler if provided, otherwise fall back to modalManager.closeSettings
|
||||||
const handleSettingsClose = onSettingsClose ?? modalManager.closeSettings;
|
const handleSettingsClose = onSettingsClose ?? modalManager.closeSettings;
|
||||||
|
|
||||||
|
const handleOpenNewTask = useCallback(() => {
|
||||||
|
modalManager.openNewTask();
|
||||||
|
}, [modalManager]);
|
||||||
|
|
||||||
|
const handleOpenGitHubImport = useCallback(() => {
|
||||||
|
modalManager.openGitHubImport();
|
||||||
|
}, [modalManager]);
|
||||||
|
|
||||||
|
const handleOnboardingViewTask = useCallback((task: Task) => {
|
||||||
|
setFirstCreatedTask(null);
|
||||||
|
modalManager.closeModelOnboarding();
|
||||||
|
modalManager.openDetailTask(task);
|
||||||
|
}, [modalManager]);
|
||||||
|
|
||||||
|
const handleModalCreateWithOnboardingTracking = useCallback(
|
||||||
|
async (input: TaskCreateInput): Promise<Task> => {
|
||||||
|
const task = await taskHandlers.handleModalCreate(input);
|
||||||
|
if (modalManager.modelOnboardingOpen) {
|
||||||
|
setFirstCreatedTask(task);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
},
|
||||||
|
[taskHandlers.handleModalCreate, modalManager.modelOnboardingOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!modalManager.modelOnboardingOpen && firstCreatedTask) {
|
||||||
|
setFirstCreatedTask(null);
|
||||||
|
}
|
||||||
|
}, [modalManager.modelOnboardingOpen, firstCreatedTask]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{modalManager.detailTask && (
|
{modalManager.detailTask && (
|
||||||
@@ -193,7 +227,7 @@ export function AppModals({
|
|||||||
isOpen={modalManager.newTaskModalOpen}
|
isOpen={modalManager.newTaskModalOpen}
|
||||||
onClose={modalManager.closeNewTask}
|
onClose={modalManager.closeNewTask}
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
onCreateTask={taskHandlers.handleModalCreate}
|
onCreateTask={handleModalCreateWithOnboardingTracking}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onPlanningMode={modalManager.openPlanningWithInitialPlan}
|
onPlanningMode={modalManager.openPlanningWithInitialPlan}
|
||||||
@@ -253,6 +287,10 @@ export function AppModals({
|
|||||||
<ModelOnboardingModal
|
<ModelOnboardingModal
|
||||||
onComplete={projectActions.handleModelOnboardingComplete}
|
onComplete={projectActions.handleModelOnboardingComplete}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
|
onOpenNewTask={handleOpenNewTask}
|
||||||
|
onOpenGitHubImport={handleOpenGitHubImport}
|
||||||
|
firstCreatedTask={firstCreatedTask}
|
||||||
|
onViewTask={handleOnboardingViewTask}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -421,6 +421,10 @@ export interface ModelOnboardingModalProps {
|
|||||||
onOpenNewTask?: () => void;
|
onOpenNewTask?: () => void;
|
||||||
/** Optional callback when user wants to open GitHub import */
|
/** Optional callback when user wants to open GitHub import */
|
||||||
onOpenGitHubImport?: () => void;
|
onOpenGitHubImport?: () => void;
|
||||||
|
/** First task created from the onboarding flow, if available */
|
||||||
|
firstCreatedTask?: Task | null;
|
||||||
|
/** Optional callback when user wants to open the created task detail */
|
||||||
|
onViewTask?: (task: Task) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Outcome states for OAuth login attempts */
|
/** Outcome states for OAuth login attempts */
|
||||||
@@ -448,6 +452,8 @@ export function ModelOnboardingModal({
|
|||||||
addToast,
|
addToast,
|
||||||
onOpenNewTask,
|
onOpenNewTask,
|
||||||
onOpenGitHubImport,
|
onOpenGitHubImport,
|
||||||
|
firstCreatedTask,
|
||||||
|
onViewTask,
|
||||||
}: ModelOnboardingModalProps) {
|
}: ModelOnboardingModalProps) {
|
||||||
// Initialize from persisted state if available (allows resume from last step)
|
// Initialize from persisted state if available (allows resume from last step)
|
||||||
const persistedState = getOnboardingState();
|
const persistedState = getOnboardingState();
|
||||||
@@ -462,6 +468,7 @@ export function ModelOnboardingModal({
|
|||||||
const [step, setStep] = useState<OnboardingStep>(initialStep);
|
const [step, setStep] = useState<OnboardingStep>(initialStep);
|
||||||
const [completedSteps, setCompletedSteps] = useState<OnboardingStep[]>(persistedCompletedSteps);
|
const [completedSteps, setCompletedSteps] = useState<OnboardingStep[]>(persistedCompletedSteps);
|
||||||
const [skippedSteps, setSkippedSteps] = useState<OnboardingStep[]>(persistedSkippedSteps);
|
const [skippedSteps, setSkippedSteps] = useState<OnboardingStep[]>(persistedSkippedSteps);
|
||||||
|
const [showTaskCreated, setShowTaskCreated] = useState(false);
|
||||||
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
|
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
|
||||||
const [authLoading, setAuthLoading] = useState(true);
|
const [authLoading, setAuthLoading] = useState(true);
|
||||||
const [authActionInProgress, setAuthActionInProgress] = useState<string | null>(null);
|
const [authActionInProgress, setAuthActionInProgress] = useState<string | null>(null);
|
||||||
@@ -479,6 +486,7 @@ export function ModelOnboardingModal({
|
|||||||
return state?.stepData?.github?.skipped === true;
|
return state?.stepData?.github?.skipped === true;
|
||||||
});
|
});
|
||||||
const pollCountRef = useRef<number>(0);
|
const pollCountRef = useRef<number>(0);
|
||||||
|
const previousCreatedTaskRef = useRef<Task | null | undefined>(firstCreatedTask);
|
||||||
|
|
||||||
// Initialize skippedProviders from persisted state
|
// Initialize skippedProviders from persisted state
|
||||||
const [skippedProviders, setSkippedProviders] = useState<Record<string, boolean>>(
|
const [skippedProviders, setSkippedProviders] = useState<Record<string, boolean>>(
|
||||||
@@ -506,6 +514,21 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
}, [step, completedSteps, skippedSteps]);
|
}, [step, completedSteps, skippedSteps]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hadCreatedTask = previousCreatedTaskRef.current != null;
|
||||||
|
const hasCreatedTask = firstCreatedTask != null;
|
||||||
|
|
||||||
|
if (!hadCreatedTask && hasCreatedTask) {
|
||||||
|
setShowTaskCreated(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCreatedTask) {
|
||||||
|
setShowTaskCreated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousCreatedTaskRef.current = firstCreatedTask;
|
||||||
|
}, [firstCreatedTask]);
|
||||||
|
|
||||||
// Auto-mark unconnected providers as skipped when leaving ai-setup step
|
// Auto-mark unconnected providers as skipped when leaving ai-setup step
|
||||||
// Only skip if NO providers are connected (if at least one is connected, others remain "Not connected")
|
// Only skip if NO providers are connected (if at least one is connected, others remain "Not connected")
|
||||||
const prevStepRef = useRef<OnboardingStep>(initialStep);
|
const prevStepRef = useRef<OnboardingStep>(initialStep);
|
||||||
@@ -1138,11 +1161,9 @@ export function ModelOnboardingModal({
|
|||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal and trigger callback
|
// Keep onboarding open so task creation can hand back to a success state
|
||||||
setIsOpen(false);
|
|
||||||
onComplete();
|
|
||||||
onOpenNewTask?.();
|
onOpenNewTask?.();
|
||||||
}, [selectedModel, availableModels, onComplete, onOpenNewTask]);
|
}, [selectedModel, availableModels, onOpenNewTask]);
|
||||||
|
|
||||||
// Handle GitHub import CTA - mark complete, close modal, then open GitHub import
|
// Handle GitHub import CTA - mark complete, close modal, then open GitHub import
|
||||||
const handleOpenGitHubImport = useCallback(async () => {
|
const handleOpenGitHubImport = useCallback(async () => {
|
||||||
@@ -1204,6 +1225,19 @@ export function ModelOnboardingModal({
|
|||||||
onComplete();
|
onComplete();
|
||||||
}, [onComplete]);
|
}, [onComplete]);
|
||||||
|
|
||||||
|
const handleViewCreatedTask = useCallback(() => {
|
||||||
|
if (!firstCreatedTask) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewTask?.(firstCreatedTask);
|
||||||
|
onComplete();
|
||||||
|
}, [firstCreatedTask, onViewTask, onComplete]);
|
||||||
|
|
||||||
|
const handleGoToDashboard = useCallback(() => {
|
||||||
|
onComplete();
|
||||||
|
}, [onComplete]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const oauthProviders = authProviders.filter(
|
const oauthProviders = authProviders.filter(
|
||||||
@@ -1300,6 +1334,9 @@ export function ModelOnboardingModal({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstCreatedTaskPreview =
|
||||||
|
firstCreatedTask?.description?.split("\n")[0]?.trim() || firstCreatedTask?.title || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal-overlay open"
|
className="modal-overlay open"
|
||||||
@@ -1820,54 +1857,86 @@ export function ModelOnboardingModal({
|
|||||||
Your workspace is ready. Here's how to get started:
|
Your workspace is ready. Here's how to get started:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ReadinessSummary items={readinessItems} />
|
{showTaskCreated && firstCreatedTask ? (
|
||||||
|
<div className="onboarding-task-created">
|
||||||
<OnboardingDisclosure summary="What happens when I create a task?">
|
<CheckCircle size={56} className="success-icon" />
|
||||||
<p className="onboarding-helper-text">
|
<h3 className="onboarding-task-created__title">Your first task is ready!</h3>
|
||||||
A task describes something you want done. Fusion's AI agents will read
|
<div className="onboarding-task-created__task-id">{firstCreatedTask.id}</div>
|
||||||
your description and work on implementing it. You can track progress on
|
{firstCreatedTaskPreview && (
|
||||||
the board and review the results.
|
<p className="onboarding-task-created__description">{firstCreatedTaskPreview}</p>
|
||||||
</p>
|
)}
|
||||||
</OnboardingDisclosure>
|
<p className="onboarding-task-created__hint">
|
||||||
|
Your task has been created and will appear on the board.
|
||||||
<div className="onboarding-cta-options">
|
</p>
|
||||||
<button
|
<div className="onboarding-task-created__actions">
|
||||||
className="onboarding-cta-card primary"
|
<button
|
||||||
onClick={handleOpenNewTask}
|
type="button"
|
||||||
disabled={saving}
|
className="btn btn-primary"
|
||||||
>
|
onClick={handleViewCreatedTask}
|
||||||
<div className="cta-icon">
|
>
|
||||||
<Plus size={24} />
|
View Task
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
onClick={handleGoToDashboard}
|
||||||
|
>
|
||||||
|
Go to Dashboard
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="cta-content">
|
</div>
|
||||||
<strong>Create a New Task</strong>
|
) : (
|
||||||
<span>Describe what you need built and AI will work on it</span>
|
<>
|
||||||
</div>
|
<ReadinessSummary items={readinessItems} />
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<OnboardingDisclosure summary="What happens when I create a task?">
|
||||||
className={`onboarding-cta-card${!isGithubAuthenticated ? " onboarding-cta-card--disabled" : ""}`}
|
<p className="onboarding-helper-text">
|
||||||
data-testid="cta-github-import"
|
A task describes something you want done. Fusion's AI agents will read
|
||||||
onClick={handleOpenGitHubImport}
|
your description and work on implementing it. You can track progress on
|
||||||
disabled={saving}
|
the board and review the results.
|
||||||
>
|
</p>
|
||||||
<div className="cta-icon">
|
</OnboardingDisclosure>
|
||||||
<GitPullRequest size={24} />
|
|
||||||
</div>
|
|
||||||
<div className="cta-content">
|
|
||||||
<strong>Import from GitHub</strong>
|
|
||||||
<span>Turn GitHub issues into tasks you can track here</span>
|
|
||||||
{!isGithubAuthenticated && (
|
|
||||||
<small className="onboarding-cta-note">Requires GitHub connection</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="onboarding-skip-note">
|
<div className="onboarding-cta-options">
|
||||||
You can create tasks anytime from the board, or use{" "}
|
<button
|
||||||
<code>fn task create</code> in the terminal.
|
className="onboarding-cta-card primary"
|
||||||
</p>
|
onClick={handleOpenNewTask}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<div className="cta-icon">
|
||||||
|
<Plus size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="cta-content">
|
||||||
|
<strong>Create a New Task</strong>
|
||||||
|
<span>Describe what you need built and AI will work on it</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`onboarding-cta-card${!isGithubAuthenticated ? " onboarding-cta-card--disabled" : ""}`}
|
||||||
|
data-testid="cta-github-import"
|
||||||
|
onClick={handleOpenGitHubImport}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<div className="cta-icon">
|
||||||
|
<GitPullRequest size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="cta-content">
|
||||||
|
<strong>Import from GitHub</strong>
|
||||||
|
<span>Turn GitHub issues into tasks you can track here</span>
|
||||||
|
{!isGithubAuthenticated && (
|
||||||
|
<small className="onboarding-cta-note">Requires GitHub connection</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="onboarding-skip-note">
|
||||||
|
You can create tasks anytime from the board, or use{" "}
|
||||||
|
<code>fn task create</code> in the terminal.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1916,7 +1985,7 @@ export function ModelOnboardingModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === "first-task" && (
|
{step === "first-task" && !showTaskCreated && (
|
||||||
<>
|
<>
|
||||||
<button className="btn btn-sm" onClick={handleBack}>
|
<button className="btn btn-sm" onClick={handleBack}>
|
||||||
← Back
|
← Back
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||||||
import { render, screen, fireEvent, waitFor, act, within } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor, act, within } from "@testing-library/react";
|
||||||
import { ModelOnboardingModal } from "../ModelOnboardingModal";
|
import { ModelOnboardingModal } from "../ModelOnboardingModal";
|
||||||
import type { AuthProvider } from "../../api";
|
import type { AuthProvider } from "../../api";
|
||||||
|
import type { Task } from "@fusion/core";
|
||||||
|
|
||||||
// Mock the API module
|
// Mock the API module
|
||||||
const mockFetchAuthStatus = vi.fn();
|
const mockFetchAuthStatus = vi.fn();
|
||||||
@@ -94,6 +95,12 @@ const defaultModels = [
|
|||||||
{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128000 },
|
{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const createdTaskMock = {
|
||||||
|
id: "FN-0001",
|
||||||
|
title: "Initial task",
|
||||||
|
description: "Implement onboarding success flow\nAdditional details that should not render",
|
||||||
|
} as unknown as Task;
|
||||||
|
|
||||||
// Navigate through steps helper
|
// Navigate through steps helper
|
||||||
async function navigateToGitHubStep() {
|
async function navigateToGitHubStep() {
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -1225,6 +1232,229 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(screen.getByText("Import from GitHub")).toBeTruthy();
|
expect(screen.getByText("Import from GitHub")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows task-created success view after firstCreatedTask transitions from null", async () => {
|
||||||
|
mockGetOnboardingState.mockReturnValue({
|
||||||
|
currentStep: "first-task",
|
||||||
|
completedSteps: ["ai-setup", "github"],
|
||||||
|
skippedSteps: [],
|
||||||
|
updatedAt: "2026-04-17T00:00:00.000Z",
|
||||||
|
dismissed: false,
|
||||||
|
completed: false,
|
||||||
|
stepData: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
firstCreatedTask={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
firstCreatedTask={createdTaskMock}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Your first task is ready!")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText("Create a New Task")).toBeNull();
|
||||||
|
expect(screen.getByText(createdTaskMock.id)).toBeTruthy();
|
||||||
|
expect(screen.getByText("Implement onboarding success flow")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the created task ID in the success view", async () => {
|
||||||
|
mockGetOnboardingState.mockReturnValue({
|
||||||
|
currentStep: "first-task",
|
||||||
|
completedSteps: ["ai-setup", "github"],
|
||||||
|
skippedSteps: [],
|
||||||
|
updatedAt: "2026-04-17T00:00:00.000Z",
|
||||||
|
dismissed: false,
|
||||||
|
completed: false,
|
||||||
|
stepData: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
firstCreatedTask={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
firstCreatedTask={createdTaskMock}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("FN-0001")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the first line of created task description in the success view", async () => {
|
||||||
|
mockGetOnboardingState.mockReturnValue({
|
||||||
|
currentStep: "first-task",
|
||||||
|
completedSteps: ["ai-setup", "github"],
|
||||||
|
skippedSteps: [],
|
||||||
|
updatedAt: "2026-04-17T00:00:00.000Z",
|
||||||
|
dismissed: false,
|
||||||
|
completed: false,
|
||||||
|
stepData: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
firstCreatedTask={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
firstCreatedTask={createdTaskMock}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Implement onboarding success flow")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("View Task button calls onViewTask with task and then onComplete", async () => {
|
||||||
|
mockGetOnboardingState.mockReturnValue({
|
||||||
|
currentStep: "first-task",
|
||||||
|
completedSteps: ["ai-setup", "github"],
|
||||||
|
skippedSteps: [],
|
||||||
|
updatedAt: "2026-04-17T00:00:00.000Z",
|
||||||
|
dismissed: false,
|
||||||
|
completed: false,
|
||||||
|
stepData: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onComplete = vi.fn();
|
||||||
|
const onViewTask = vi.fn();
|
||||||
|
const { rerender } = render(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={onComplete}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
onViewTask={onViewTask}
|
||||||
|
firstCreatedTask={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={onComplete}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
onViewTask={onViewTask}
|
||||||
|
firstCreatedTask={createdTaskMock}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("button", { name: "View Task" })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "View Task" }));
|
||||||
|
|
||||||
|
expect(onViewTask).toHaveBeenCalledWith(createdTaskMock);
|
||||||
|
expect(onComplete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Go to Dashboard button calls onComplete", async () => {
|
||||||
|
mockGetOnboardingState.mockReturnValue({
|
||||||
|
currentStep: "first-task",
|
||||||
|
completedSteps: ["ai-setup", "github"],
|
||||||
|
skippedSteps: [],
|
||||||
|
updatedAt: "2026-04-17T00:00:00.000Z",
|
||||||
|
dismissed: false,
|
||||||
|
completed: false,
|
||||||
|
stepData: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onComplete = vi.fn();
|
||||||
|
const onViewTask = vi.fn();
|
||||||
|
const { rerender } = render(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={onComplete}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
onViewTask={onViewTask}
|
||||||
|
firstCreatedTask={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ModelOnboardingModal
|
||||||
|
onComplete={onComplete}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
onViewTask={onViewTask}
|
||||||
|
firstCreatedTask={createdTaskMock}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("button", { name: "Go to Dashboard" })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Go to Dashboard" }));
|
||||||
|
|
||||||
|
expect(onComplete).toHaveBeenCalled();
|
||||||
|
expect(onViewTask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps CTA cards visible and success hidden when firstCreatedTask is not provided", async () => {
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} firstCreatedTask={null} />);
|
||||||
|
|
||||||
|
await navigateToFirstTaskStep();
|
||||||
|
|
||||||
|
expect(screen.getByText("Create a New Task")).toBeTruthy();
|
||||||
|
expect(screen.queryByText("Your first task is ready!")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Finish Setup still transitions to complete step when no task is created", async () => {
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} firstCreatedTask={null} />);
|
||||||
|
|
||||||
|
await navigateToFirstTaskStep();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Finish Setup"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("All Set!")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("Import from GitHub card shows connection note when GitHub not connected", async () => {
|
it("Import from GitHub card shows connection note when GitHub not connected", async () => {
|
||||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
@@ -1306,7 +1536,7 @@ describe("ModelOnboardingModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("completion", () => {
|
describe("completion", () => {
|
||||||
it("completes onboarding and calls onOpenNewTask callback", async () => {
|
it("marks onboarding complete and opens New Task without closing onboarding immediately", async () => {
|
||||||
const onComplete = vi.fn();
|
const onComplete = vi.fn();
|
||||||
const onOpenNewTask = vi.fn();
|
const onOpenNewTask = vi.fn();
|
||||||
|
|
||||||
@@ -1348,9 +1578,10 @@ describe("ModelOnboardingModal", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should close modal and call both callbacks
|
// Should open New Task flow but keep onboarding open for success handoff
|
||||||
expect(onComplete).toHaveBeenCalled();
|
|
||||||
expect(onOpenNewTask).toHaveBeenCalled();
|
expect(onOpenNewTask).toHaveBeenCalled();
|
||||||
|
expect(onComplete).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("completes onboarding and calls onOpenGitHubImport callback", async () => {
|
it("completes onboarding and calls onOpenGitHubImport callback", async () => {
|
||||||
|
|||||||
@@ -24499,6 +24499,65 @@ html .column.drag-over * {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Onboarding Task Created Success === */
|
||||||
|
.onboarding-task-created {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-xl) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created .success-icon {
|
||||||
|
color: var(--color-success);
|
||||||
|
filter: drop-shadow(var(--glow-success));
|
||||||
|
width: calc(var(--space-xl) + var(--space-2xl));
|
||||||
|
height: calc(var(--space-xl) + var(--space-2xl));
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: calc(var(--space-lg) + (var(--space-xs) / 2));
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created__task-id {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: calc(var(--space-md) + (var(--space-xs) / 2));
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--surface);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created__description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: calc(var(--space-md) + (var(--space-xs) / 4));
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: calc(var(--space-xl) * 15);
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created__hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: calc(var(--space-md) + (var(--space-xs) / 4));
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Onboarding Resume Card */
|
/* Onboarding Resume Card */
|
||||||
.onboarding-resume-card {
|
.onboarding-resume-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24794,6 +24853,15 @@ html .column.drag-over * {
|
|||||||
.onboarding-cta-card .cta-content span {
|
.onboarding-cta-card .cta-content span {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created__actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-task-created__actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user