feat(FN-1902): merge fusion/fn-1902
This commit is contained in:
@@ -690,6 +690,14 @@ export function ModelOnboardingModal({
|
||||
const aiOauthProviders = oauthProviders.filter((p) => p.id !== "github");
|
||||
const aiApiKeyProviders = apiKeyProviders.filter((p) => p.id !== "github");
|
||||
|
||||
// Skip-state detection: derived state for informational banners
|
||||
// Detects whether at least one AI provider is connected (excludes GitHub)
|
||||
const hasAiProvider = authProviders.some((p) => p.id !== "github" && p.authenticated);
|
||||
// True when on GitHub step but skipped AI setup (no AI provider connected)
|
||||
const aiSetupSkipped = step === "github" && !hasAiProvider;
|
||||
// True when on First Task step but skipped GitHub
|
||||
const githubSkipped = step === "first-task" && !isGithubAuthenticated;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay open"
|
||||
@@ -1078,6 +1086,17 @@ export function ModelOnboardingModal({
|
||||
Fusion works without it.
|
||||
</p>
|
||||
|
||||
{/* Skip-state banner: shown when AI setup was skipped */}
|
||||
{aiSetupSkipped && (
|
||||
<div className="onboarding-skip-banner" role="status">
|
||||
<strong>No AI provider connected</strong>
|
||||
<p>
|
||||
AI features like task planning and code generation won't be available until you connect one.
|
||||
You can set this up later in Settings.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OnboardingDisclosure summary="What does GitHub integration do?">
|
||||
<p className="onboarding-helper-text">
|
||||
Connecting GitHub lets you import issues as tasks, track pull requests,
|
||||
@@ -1178,6 +1197,28 @@ export function ModelOnboardingModal({
|
||||
Your workspace is ready. Here's how to get started:
|
||||
</p>
|
||||
|
||||
{/* Skip-state banner: shown when GitHub was skipped */}
|
||||
{githubSkipped && (
|
||||
<div className="onboarding-skip-banner" role="status">
|
||||
<strong>GitHub not connected</strong>
|
||||
<p>
|
||||
You won't be able to import issues from GitHub, but you can still create tasks manually.
|
||||
Connect GitHub later from Settings.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skip-state banner: shown when AI setup was also skipped */}
|
||||
{!hasAiProvider && (
|
||||
<div className="onboarding-skip-banner" role="status">
|
||||
<strong>No AI provider connected</strong>
|
||||
<p>
|
||||
AI agents won't be able to work on tasks until you connect a provider.
|
||||
Set one up later in Settings → AI Setup.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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
|
||||
|
||||
@@ -2104,4 +2104,147 @@ describe("ModelOnboardingModal progressive disclosure", () => {
|
||||
expect(screen.getByTestId("onboarding-auth-status-github")).toHaveClass("auth-status-badge");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Skip-state messaging", () => {
|
||||
it("shows AI provider skip banner on GitHub step when no provider connected", async () => {
|
||||
// Start at ai-setup with no AI providers connected
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
// Should show skip banner about AI provider
|
||||
const banners = screen.getAllByRole("status");
|
||||
expect(banners.some(b => b.classList.contains("onboarding-skip-banner"))).toBe(true);
|
||||
|
||||
const skipBanner = screen.getByText("No AI provider connected").closest(".onboarding-skip-banner");
|
||||
expect(skipBanner).toBeTruthy();
|
||||
expect(skipBanner).toHaveTextContent(/AI features like task planning and code generation won't be available/);
|
||||
});
|
||||
|
||||
it("does not show AI provider skip banner on GitHub step when provider is connected", async () => {
|
||||
// Start with an AI provider already connected
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: true, type: "oauth" },
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
// Should NOT show skip banner about AI provider
|
||||
expect(screen.queryByText("No AI provider connected")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows GitHub skip banner on First Task step when GitHub not connected", async () => {
|
||||
// AI provider connected but GitHub not connected
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: true, type: "oauth" },
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToFirstTaskStep();
|
||||
|
||||
// Should show skip banner about GitHub
|
||||
const skipBanner = screen.getByText("GitHub not connected").closest(".onboarding-skip-banner");
|
||||
expect(skipBanner).toBeTruthy();
|
||||
expect(skipBanner).toHaveTextContent(/won't be able to import issues from GitHub/);
|
||||
});
|
||||
|
||||
it("does not show GitHub skip banner on First Task step when GitHub is connected", async () => {
|
||||
// Both AI and GitHub connected - mock for all auth status calls
|
||||
mockFetchAuthStatus.mockResolvedValue({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: true, type: "oauth" },
|
||||
{ id: "github", name: "GitHub", authenticated: true, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToFirstTaskStep();
|
||||
|
||||
// Should NOT show skip banner about GitHub (use querySelector to avoid matching the auth badge)
|
||||
const skipBanners = document.querySelectorAll(".onboarding-skip-banner");
|
||||
const githubSkipBanner = Array.from(skipBanners).find(
|
||||
(banner) => banner.textContent?.includes("GitHub not connected")
|
||||
);
|
||||
expect(githubSkipBanner).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shows both skip banners on First Task step when both AI and GitHub are skipped", async () => {
|
||||
// Neither AI nor GitHub connected
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToFirstTaskStep();
|
||||
|
||||
// Should show BOTH skip banners
|
||||
expect(screen.getByText("GitHub not connected")).toBeTruthy();
|
||||
expect(screen.getByText("No AI provider connected")).toBeTruthy();
|
||||
|
||||
// Both should have the skip-banner class
|
||||
const githubBanner = screen.getByText("GitHub not connected").closest(".onboarding-skip-banner");
|
||||
const aiBanner = screen.getByText("No AI provider connected").closest(".onboarding-skip-banner");
|
||||
expect(githubBanner).toBeTruthy();
|
||||
expect(aiBanner).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show any skip banner on AI Setup step", async () => {
|
||||
// No providers connected
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Should NOT show any skip banners on AI Setup step
|
||||
expect(screen.queryByText("onboarding-skip-banner")).toBeNull();
|
||||
const skipBanners = document.querySelectorAll(".onboarding-skip-banner");
|
||||
expect(skipBanners.length).toBe(0);
|
||||
});
|
||||
|
||||
it("skip banners have role=status for accessibility", async () => {
|
||||
// Set up with no AI provider connected
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
// Check that skip banner has role="status"
|
||||
const skipBanner = screen.getByText("No AI provider connected").closest(".onboarding-skip-banner");
|
||||
expect(skipBanner).toHaveAttribute("role", "status");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23536,6 +23536,31 @@ html .column.drag-over * {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* === Skip-state informational banners === */
|
||||
.onboarding-skip-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 3px solid var(--color-warning);
|
||||
background: color-mix(in srgb, var(--color-warning) 8%, transparent);
|
||||
}
|
||||
|
||||
.onboarding-skip-banner strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.onboarding-skip-banner p {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* === OnboardingDisclosure === */
|
||||
.onboarding-disclosure {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user