feat(FN-2318): merge fusion/fn-2318
This commit is contained in:
@@ -415,7 +415,7 @@ View a centralized timeline of all task lifecycle events. Click the history icon
|
||||
- **Settings API Contract**: Server-owned fields like `githubTokenConfigured` are injected on GET /settings but stripped on PUT /settings to prevent persistence to config.json
|
||||
- **Notifications**: ntfy.sh integration for push notifications when tasks complete or fail
|
||||
- **Authentication**: Provider management for AI model access. OAuth providers (e.g. Anthropic) use a Login/Logout flow; API-key providers (e.g. OpenRouter) show a masked key entry with Save/Clear actions. After saving or clearing a key, the auth status refreshes immediately so the authenticated badge stays in sync. Stored key values are never prefilled or displayed.
|
||||
- **Onboarding Wizard**: On first dashboard launch, a multi-step onboarding wizard guides users through three steps: (1) AI Setup — authenticate with a provider and select a default model, (2) GitHub (optional) — connect GitHub for issue import and PR management, and (3) First Task — create your first task or import from GitHub. The wizard is dismissible and non-blocking — completion is tracked via the `modelOnboardingComplete` global setting. Users can skip onboarding or re-trigger it by clearing the flag in Settings.
|
||||
- **Onboarding Wizard**: On first dashboard launch, a multi-step onboarding wizard guides users through three steps: (1) AI Setup — authenticate with a provider and select a default model, (2) GitHub (optional) — connect GitHub for issue import and PR management, and (3) First Task — create your first task or import from GitHub. If there is no active project, the First Task step blocks task/import actions and prompts the user to open the existing project setup wizard first. The wizard is dismissible and non-blocking — completion is tracked via the `modelOnboardingComplete` global setting. Users can skip onboarding or re-trigger it by clearing the flag in Settings.
|
||||
- **Pause Controls**: Soft pause (stop new work) and hard stop (kill all agents)
|
||||
- **Theming**: Light/dark/system mode toggle and 12 color themes (see Theming section below)
|
||||
|
||||
|
||||
@@ -805,7 +805,7 @@ function AppInner() {
|
||||
toasts={toasts}
|
||||
removeToast={removeToast}
|
||||
modalManager={modalManager}
|
||||
projectActions={{ handleSetupComplete, handleModelOnboardingComplete }}
|
||||
projectActions={{ handleAddProject, handleSetupComplete, handleModelOnboardingComplete }}
|
||||
taskHandlers={{
|
||||
handleModalCreate,
|
||||
handlePlanningTaskCreated,
|
||||
|
||||
@@ -34,7 +34,7 @@ interface AppModalsProps {
|
||||
toasts: Toast[];
|
||||
removeToast: (id: number) => void;
|
||||
modalManager: ModalManager;
|
||||
projectActions: Pick<UseProjectActionsResult, "handleSetupComplete" | "handleModelOnboardingComplete">;
|
||||
projectActions: Pick<UseProjectActionsResult, "handleAddProject" | "handleSetupComplete" | "handleModelOnboardingComplete">;
|
||||
taskHandlers: Pick<UseTaskHandlersResult, "handleModalCreate" | "handlePlanningTaskCreated" | "handlePlanningTasksCreated" | "handleSubtaskTasksCreated" | "handleGitHubImport">;
|
||||
taskOperations: {
|
||||
moveTask: (taskId: string, column: Column, position?: number) => Promise<Task>;
|
||||
@@ -287,6 +287,8 @@ export function AppModals({
|
||||
<ModelOnboardingModal
|
||||
onComplete={projectActions.handleModelOnboardingComplete}
|
||||
addToast={addToast}
|
||||
projectId={projectId ?? ""}
|
||||
onOpenSetupWizard={projectActions.handleAddProject}
|
||||
onOpenNewTask={handleOpenNewTask}
|
||||
onOpenGitHubImport={handleOpenGitHubImport}
|
||||
firstCreatedTask={firstCreatedTask}
|
||||
|
||||
@@ -420,6 +420,10 @@ export interface ModelOnboardingModalProps {
|
||||
onComplete: () => void;
|
||||
/** Toast helper */
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
/** Currently selected project ID (required for first-task actions) */
|
||||
projectId?: string;
|
||||
/** Optional callback to open project setup wizard when no project is selected */
|
||||
onOpenSetupWizard?: () => void;
|
||||
/** Optional callback when user wants to open new task creation */
|
||||
onOpenNewTask?: () => void;
|
||||
/** Optional callback when user wants to open GitHub import */
|
||||
@@ -458,6 +462,8 @@ const MAX_POLL_CYCLES = 150;
|
||||
export function ModelOnboardingModal({
|
||||
onComplete,
|
||||
addToast,
|
||||
projectId,
|
||||
onOpenSetupWizard,
|
||||
onOpenNewTask,
|
||||
onOpenGitHubImport,
|
||||
firstCreatedTask,
|
||||
@@ -1216,6 +1222,10 @@ export function ModelOnboardingModal({
|
||||
|
||||
// Handle GitHub import CTA - mark complete, close modal, then open GitHub import
|
||||
const handleOpenGitHubImport = useCallback(async () => {
|
||||
if (!isGitHubReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First complete the onboarding
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -1229,7 +1239,7 @@ export function ModelOnboardingModal({
|
||||
onComplete();
|
||||
trackOnboardingEvent("onboarding:open-github-import", {});
|
||||
onOpenGitHubImport?.();
|
||||
}, [completeOnboarding, onComplete, onOpenGitHubImport]);
|
||||
}, [completeOnboarding, isGitHubReady, onComplete, onOpenGitHubImport]);
|
||||
|
||||
// Dismiss without completing (still marks onboarding complete)
|
||||
const handleDismiss = useCallback(async () => {
|
||||
@@ -1288,6 +1298,7 @@ export function ModelOnboardingModal({
|
||||
const aiProviders = authProviders.filter((provider) => provider.id !== "github");
|
||||
const connectedAiProviders = aiProviders.filter((provider) => provider.authenticated);
|
||||
const hasAiProvider = connectedAiProviders.length > 0;
|
||||
const hasProjectSelected = Boolean(projectId);
|
||||
// True when on GitHub step but skipped AI setup (no AI provider connected)
|
||||
const aiSetupSkipped = step === "github" && !hasAiProvider;
|
||||
|
||||
@@ -1316,6 +1327,20 @@ export function ModelOnboardingModal({
|
||||
|
||||
const readinessItems: ReadinessItem[] = [];
|
||||
|
||||
if (hasProjectSelected) {
|
||||
readinessItems.push({
|
||||
label: "Project",
|
||||
status: "connected",
|
||||
detail: "Project selected — task creation and imports are available",
|
||||
});
|
||||
} else {
|
||||
readinessItems.push({
|
||||
label: "Project",
|
||||
status: "missing",
|
||||
detail: "Register a project to enable task creation and imports",
|
||||
});
|
||||
}
|
||||
|
||||
if (hasAiProvider) {
|
||||
const firstConnectedProviderName = getProviderDisplayName(connectedAiProviders[0]?.id ?? "");
|
||||
readinessItems.push({
|
||||
@@ -1906,7 +1931,9 @@ export function ModelOnboardingModal({
|
||||
{step === "first-task" && (
|
||||
<div className="model-onboarding-first-task">
|
||||
<p className="model-onboarding-description">
|
||||
Your workspace is ready. Here's how to get started:
|
||||
{hasProjectSelected
|
||||
? "Your workspace is ready. Here's how to get started:"
|
||||
: "Before creating your first task, register a project directory."}
|
||||
</p>
|
||||
|
||||
{showTaskCreated && createdTaskForDisplay ? (
|
||||
@@ -1939,97 +1966,119 @@ export function ModelOnboardingModal({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="onboarding-first-task-form">
|
||||
<label className="onboarding-first-task-form__label" htmlFor="onboarding-first-task-input">
|
||||
Describe your first task
|
||||
</label>
|
||||
<textarea
|
||||
id="onboarding-first-task-input"
|
||||
className="input onboarding-first-task-form__input"
|
||||
data-testid="onboarding-first-task-input"
|
||||
value={firstTaskDescription}
|
||||
onChange={(event) => {
|
||||
setFirstTaskDescription(event.target.value);
|
||||
setTaskCreationError(null);
|
||||
}}
|
||||
placeholder="Example: Build a login page with email and password"
|
||||
rows={4}
|
||||
/>
|
||||
<div className="onboarding-first-task-form__actions">
|
||||
{!hasProjectSelected ? (
|
||||
<div className="onboarding-project-prerequisite" data-testid="onboarding-project-prerequisite">
|
||||
<p className="onboarding-helper-text">
|
||||
A project must be selected before you can create tasks or import from GitHub.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleCreateFirstTask}
|
||||
disabled={isCreatingFirstTask}
|
||||
data-testid="onboarding-first-task-submit"
|
||||
onClick={onOpenSetupWizard}
|
||||
data-testid="onboarding-open-setup-wizard"
|
||||
>
|
||||
{isCreatingFirstTask ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>Creating task…</span>
|
||||
</>
|
||||
) : (
|
||||
"Create First Task"
|
||||
)}
|
||||
Set Up Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="onboarding-first-task-form">
|
||||
<label className="onboarding-first-task-form__label" htmlFor="onboarding-first-task-input">
|
||||
Describe your first task
|
||||
</label>
|
||||
<textarea
|
||||
id="onboarding-first-task-input"
|
||||
className="input onboarding-first-task-form__input"
|
||||
data-testid="onboarding-first-task-input"
|
||||
value={firstTaskDescription}
|
||||
onChange={(event) => {
|
||||
setFirstTaskDescription(event.target.value);
|
||||
setTaskCreationError(null);
|
||||
}}
|
||||
placeholder="Example: Build a login page with email and password"
|
||||
rows={4}
|
||||
/>
|
||||
<div className="onboarding-first-task-form__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleCreateFirstTask}
|
||||
disabled={isCreatingFirstTask}
|
||||
data-testid="onboarding-first-task-submit"
|
||||
>
|
||||
{isCreatingFirstTask ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>Creating task…</span>
|
||||
</>
|
||||
) : (
|
||||
"Create First Task"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{taskCreationError && (
|
||||
<div className="onboarding-task-error" role="alert" data-testid="onboarding-task-error">
|
||||
<p className="field-error">{taskCreationError}</p>
|
||||
<p className="onboarding-helper-text">Your text has been preserved — fix the issue and try again.</p>
|
||||
</div>
|
||||
{taskCreationError && (
|
||||
<div className="onboarding-task-error" role="alert" data-testid="onboarding-task-error">
|
||||
<p className="field-error">{taskCreationError}</p>
|
||||
<p className="onboarding-helper-text">Your text has been preserved — fix the issue and try again.</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ReadinessSummary items={readinessItems} />
|
||||
|
||||
<OnboardingDisclosure summary="What happens when I create a task?">
|
||||
<p className="onboarding-helper-text">
|
||||
A task describes something you want done. Fusion's AI agents will read
|
||||
your description and work on implementing it. You can track progress on
|
||||
the board and review the results.
|
||||
</p>
|
||||
</OnboardingDisclosure>
|
||||
{hasProjectSelected && (
|
||||
<>
|
||||
<OnboardingDisclosure summary="What happens when I create a task?">
|
||||
<p className="onboarding-helper-text">
|
||||
A task describes something you want done. Fusion's AI agents will read
|
||||
your description and work on implementing it. You can track progress on
|
||||
the board and review the results.
|
||||
</p>
|
||||
</OnboardingDisclosure>
|
||||
|
||||
<div className="onboarding-cta-options">
|
||||
<button
|
||||
className="onboarding-cta-card primary"
|
||||
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>
|
||||
<div className="onboarding-cta-options">
|
||||
<button
|
||||
className="onboarding-cta-card primary"
|
||||
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${!isGitHubReady ? " onboarding-cta-card--disabled" : ""}`}
|
||||
data-testid="cta-github-import"
|
||||
onClick={handleOpenGitHubImport}
|
||||
disabled={saving}
|
||||
>
|
||||
<div className="cta-icon">
|
||||
<GitPullRequest size={24} />
|
||||
<button
|
||||
className={`onboarding-cta-card${!isGitHubReady ? " onboarding-cta-card--disabled" : ""}`}
|
||||
data-testid="cta-github-import"
|
||||
onClick={handleOpenGitHubImport}
|
||||
disabled={saving || !isGitHubReady}
|
||||
>
|
||||
<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>
|
||||
{!isGitHubReady && (
|
||||
<small className="onboarding-cta-note">Requires GitHub connection</small>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className="cta-content">
|
||||
<strong>Import from GitHub</strong>
|
||||
<span>Turn GitHub issues into tasks you can track here</span>
|
||||
{!isGitHubReady && (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -74,8 +74,12 @@ vi.mock("../SetupWizardModal", () => ({
|
||||
SetupWizardModal: () => null,
|
||||
}));
|
||||
|
||||
const mockModelOnboardingModalProps = vi.fn();
|
||||
vi.mock("../ModelOnboardingModal", () => ({
|
||||
ModelOnboardingModal: () => null,
|
||||
ModelOnboardingModal: (props: any) => {
|
||||
mockModelOnboardingModalProps(props);
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../ToastContainer", () => ({
|
||||
@@ -184,6 +188,7 @@ describe("AppModals", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockScheduledTasksModalProps.mockClear();
|
||||
mockModelOnboardingModalProps.mockClear();
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
@@ -197,7 +202,7 @@ describe("AppModals", () => {
|
||||
toasts={mockToasts}
|
||||
removeToast={vi.fn()}
|
||||
modalManager={mockModalManager}
|
||||
projectActions={{ handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
projectActions={{ handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
|
||||
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
|
||||
deepLink={{ handleDetailClose: vi.fn() }}
|
||||
@@ -207,6 +212,36 @@ describe("AppModals", () => {
|
||||
expect(document.body).toBeDefined();
|
||||
});
|
||||
|
||||
describe("ModelOnboardingModal wiring", () => {
|
||||
it("passes project context and setup-wizard callback into onboarding modal", () => {
|
||||
const handleAddProject = vi.fn();
|
||||
const manager = { ...mockModalManager, modelOnboardingOpen: true };
|
||||
|
||||
render(
|
||||
<AppModals
|
||||
projectId={undefined}
|
||||
tasks={[]}
|
||||
projects={[]}
|
||||
currentProject={null}
|
||||
addToast={vi.fn()}
|
||||
toasts={mockToasts}
|
||||
removeToast={vi.fn()}
|
||||
modalManager={manager}
|
||||
projectActions={{ handleAddProject, handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
|
||||
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
|
||||
deepLink={{ handleDetailClose: vi.fn() }}
|
||||
settings={mockSettings}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(mockModelOnboardingModalProps).toHaveBeenCalledTimes(1);
|
||||
const props = mockModelOnboardingModalProps.mock.calls[0][0];
|
||||
expect(props.projectId).toBe("");
|
||||
expect(props.onOpenSetupWizard).toBe(handleAddProject);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ScheduledTasksModal projectId forwarding", () => {
|
||||
it("does not render ScheduledTasksModal when schedulesOpen is false", () => {
|
||||
const manager = { ...mockModalManager, schedulesOpen: false };
|
||||
@@ -220,7 +255,7 @@ describe("AppModals", () => {
|
||||
toasts={mockToasts}
|
||||
removeToast={vi.fn()}
|
||||
modalManager={manager}
|
||||
projectActions={{ handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
projectActions={{ handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
|
||||
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
|
||||
deepLink={{ handleDetailClose: vi.fn() }}
|
||||
@@ -242,7 +277,7 @@ describe("AppModals", () => {
|
||||
toasts={mockToasts}
|
||||
removeToast={vi.fn()}
|
||||
modalManager={manager}
|
||||
projectActions={{ handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
projectActions={{ handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
|
||||
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
|
||||
deepLink={{ handleDetailClose: vi.fn() }}
|
||||
@@ -266,7 +301,7 @@ describe("AppModals", () => {
|
||||
toasts={mockToasts}
|
||||
removeToast={vi.fn()}
|
||||
modalManager={manager}
|
||||
projectActions={{ handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
projectActions={{ handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
|
||||
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
|
||||
deepLink={{ handleDetailClose: vi.fn() }}
|
||||
@@ -290,7 +325,7 @@ describe("AppModals", () => {
|
||||
toasts={mockToasts}
|
||||
removeToast={vi.fn()}
|
||||
modalManager={manager}
|
||||
projectActions={{ handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
projectActions={{ handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() }}
|
||||
taskHandlers={{ handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() }}
|
||||
taskOperations={{ moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() }}
|
||||
deepLink={{ handleDetailClose: vi.fn() }}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,7 @@ interface RenderModalOverrides {
|
||||
onOpenGitHubImport?: () => void;
|
||||
onViewTask?: (task: Task) => void;
|
||||
firstCreatedTask?: Task | null;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
interface RenderModalResult extends RenderResult {
|
||||
@@ -155,6 +156,7 @@ function renderModal(overrides: RenderModalOverrides = {}): RenderModalResult {
|
||||
<ModelOnboardingModal
|
||||
onComplete={onComplete}
|
||||
addToast={addToast}
|
||||
projectId={overrides.projectId ?? "proj_123"}
|
||||
onOpenNewTask={onOpenNewTask}
|
||||
onOpenGitHubImport={onOpenGitHubImport}
|
||||
onViewTask={onViewTask}
|
||||
@@ -263,7 +265,7 @@ function AuthOnboardingHarness({ openModelOnboardingSpy, openSettingsSpy }: Auth
|
||||
return (
|
||||
<>
|
||||
{modelOnboardingOpen ? (
|
||||
<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />
|
||||
<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />
|
||||
) : (
|
||||
<div data-testid="onboarding-modal-closed" />
|
||||
)}
|
||||
@@ -795,6 +797,7 @@ describe("onboarding flow integration", () => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
addToast={vi.fn()}
|
||||
projectId="proj_123"
|
||||
/>
|
||||
) : (
|
||||
<div data-testid="modal-host-closed" />
|
||||
@@ -866,6 +869,13 @@ describe("onboarding flow integration", () => {
|
||||
});
|
||||
|
||||
it("task creation flow: Import from GitHub CTA completes onboarding and triggers import", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValue({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
{ id: "github", name: "GitHub", authenticated: true, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
const renderResult = renderModal();
|
||||
|
||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
||||
|
||||
@@ -25530,6 +25530,27 @@ html .column.drag-over * {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* === Onboarding Project Prerequisite === */
|
||||
.onboarding-project-prerequisite {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid color-mix(in srgb, var(--todo) 35%, var(--border));
|
||||
background: color-mix(in srgb, var(--todo) 10%, var(--surface));
|
||||
}
|
||||
|
||||
.onboarding-project-prerequisite .onboarding-helper-text {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
line-height: calc(var(--space-lg) + (var(--space-xs) / 2));
|
||||
}
|
||||
|
||||
.onboarding-project-prerequisite .btn {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* === Onboarding Task Error === */
|
||||
.onboarding-task-error {
|
||||
margin-top: var(--space-sm);
|
||||
@@ -25623,6 +25644,23 @@ html .column.drag-over * {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.onboarding-cta-card--disabled,
|
||||
.onboarding-cta-card:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.onboarding-cta-card:disabled:hover {
|
||||
border-color: var(--border);
|
||||
background: var(--surface);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.onboarding-cta-card:disabled:focus,
|
||||
.onboarding-cta-card:disabled:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.onboarding-cta-note {
|
||||
display: block;
|
||||
font-size: calc(var(--space-md) - (var(--space-xs) / 4));
|
||||
@@ -26105,6 +26143,15 @@ html .column.drag-over * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onboarding-project-prerequisite {
|
||||
padding: var(--space-md);
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.onboarding-project-prerequisite .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.onboarding-cta-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user