feat(FN-4140): land settings modal to general section by default

Adds a changeset for a patch and updates the SettingsModal to default to the "general" section on open, with tests covering the section routing behavior.

Fusion-Task-Id: FN-4140
This commit is contained in:
Fusion
2026-05-12 22:08:08 -07:00
committed by gsxdsm
parent a3d57a9c4f
commit f8066c4a06
3 changed files with 54 additions and 10 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Default the Settings modal to the General section instead of Authentication when no initial section is specified.

View File

@@ -326,6 +326,8 @@ function normalizeExperimentalFeaturesForSave(features?: Record<string, boolean>
type LegacySectionId = "pi-extensions"; type LegacySectionId = "pi-extensions";
export type SectionId = SettingsSection["id"] | LegacySectionId; export type SectionId = SettingsSection["id"] | LegacySectionId;
const DEFAULT_SETTINGS_SECTION: SectionId = "global-general";
type PluginsSubsectionId = "fusion-plugins" | "pi-extensions"; type PluginsSubsectionId = "fusion-plugins" | "pi-extensions";
/** Local form state extends Settings with a worktreeInitCommand override and lets tokenCap carry null (delete semantic). */ /** Local form state extends Settings with a worktreeInitCommand override and lets tokenCap carry null (delete semantic). */
@@ -335,7 +337,7 @@ interface SettingsModalProps {
onClose: () => void; onClose: () => void;
addToast: (message: string, type?: ToastType) => void; addToast: (message: string, type?: ToastType) => void;
projectId?: string; projectId?: string;
/** Optional section to show when the modal first opens. Defaults to first non-group-header section. */ /** Optional section to show when the modal first opens. Defaults to the global General section. */
initialSection?: SectionId; initialSection?: SectionId;
/** Current theme mode */ /** Current theme mode */
themeMode?: ThemeMode; themeMode?: ThemeMode;
@@ -414,13 +416,13 @@ export function SettingsModal({
const [scopedSettings, setScopedSettings] = useState<{ global: GlobalSettings; project: Partial<Settings> } | null>(null); const [scopedSettings, setScopedSettings] = useState<{ global: GlobalSettings; project: Partial<Settings> } | null>(null);
// Track initial scoped values for null-as-delete semantics on project overrides // Track initial scoped values for null-as-delete semantics on project overrides
const [initialScopedValues, setInitialScopedValues] = useState<{ global: GlobalSettings; project: Partial<Settings> } | null>(null); const [initialScopedValues, setInitialScopedValues] = useState<{ global: GlobalSettings; project: Partial<Settings> } | null>(null);
// Find the first non-group-header section for default active section // Find the first non-group-header section for visibility fallback handling
const firstNonHeaderSection = SETTINGS_SECTIONS.find((s) => !s.isGroupHeader); const firstNonHeaderSection = SETTINGS_SECTIONS.find((s) => !s.isGroupHeader);
const [activeSection, setActiveSection] = useState<SectionId>(() => { const [activeSection, setActiveSection] = useState<SectionId>(() => {
if (initialSection === "pi-extensions") { if (initialSection === "pi-extensions") {
return "plugins"; return "plugins";
} }
return initialSection ?? firstNonHeaderSection?.id ?? "authentication"; return initialSection ?? DEFAULT_SETTINGS_SECTION;
}); });
// Deterministic default: opening Plugins starts on Fusion Plugins unless legacy // Deterministic default: opening Plugins starts on Fusion Plugins unless legacy
// `initialSection="pi-extensions"` is explicitly provided. // `initialSection="pi-extensions"` is explicitly provided.
@@ -470,7 +472,9 @@ export function SettingsModal({
return true; return true;
}); });
const firstVisibleSectionId = visibleSections.find((section) => !section.isGroupHeader)?.id ?? "general"; const firstVisibleSectionId = visibleSections.some((section) => section.id === DEFAULT_SETTINGS_SECTION)
? DEFAULT_SETTINGS_SECTION
: (visibleSections.find((section) => !section.isGroupHeader)?.id ?? firstNonHeaderSection?.id ?? "general");
/** Get the scope of the currently active section */ /** Get the scope of the currently active section */
const activeSectionScope = visibleSections.find((s) => s.id === activeSection)?.scope; const activeSectionScope = visibleSections.find((s) => s.id === activeSection)?.scope;

View File

@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { ComponentProps } from "react";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react"; import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { SettingsModal } from "../SettingsModal"; import { SettingsModal } from "../SettingsModal";
@@ -196,11 +197,12 @@ const defaultSettings = {
webhookEvents: undefined, webhookEvents: undefined,
}; };
function renderModal(props = {}) { function renderModal(props: Partial<ComponentProps<typeof SettingsModal>> = {}) {
return render( return render(
<SettingsModal <SettingsModal
onClose={noop} onClose={noop}
addToast={noop} addToast={noop}
initialSection="authentication"
{...props} {...props}
/> />
); );
@@ -243,13 +245,46 @@ describe("SettingsModal", () => {
const authenticationHeading = screen.getByRole("heading", { name: "Authentication" }); const authenticationHeading = screen.getByRole("heading", { name: "Authentication" });
expect(authenticationHeading).toHaveClass("settings-section-heading"); expect(authenticationHeading).toHaveClass("settings-section-heading");
await userEvent.click(screen.getAllByRole("button", { name: /^General$/ })[0]); await userEvent.click(screen.getByRole("button", { name: /^General$/ }));
const generalHeading = screen.getByRole("heading", { name: "General" }); const generalHeading = screen.getByRole("heading", { name: "General" });
expect(generalHeading).toHaveClass("settings-section-heading"); expect(generalHeading).toHaveClass("settings-section-heading");
expect(container.querySelectorAll(".settings-section-heading").length).toBeGreaterThan(0); expect(container.querySelectorAll(".settings-section-heading").length).toBeGreaterThan(0);
}); });
it("defaults to the global General section when no initialSection is provided", async () => {
render(
<SettingsModal
onClose={noop}
addToast={noop}
/>,
);
await waitForSettingsModalReady();
const generalNavButton = screen.getByRole("button", { name: /^General$/ });
expect(generalNavButton).toHaveClass("active");
expect(screen.getByRole("heading", { name: "General" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /^Authentication$/ })).not.toHaveClass("active");
});
it("honors an explicit initialSection override", async () => {
renderModal({ initialSection: "authentication" });
await waitForSettingsModalReady();
expect(screen.getByRole("button", { name: /^Authentication$/ })).toHaveClass("active");
expect(screen.getByRole("heading", { name: "Authentication" })).toBeInTheDocument();
});
it("maps the legacy pi-extensions initialSection alias to Plugins", async () => {
renderModal({ initialSection: "pi-extensions" });
await waitForSettingsModalReady();
expect(screen.getByRole("button", { name: /^Plugins$/ })).toHaveClass("active");
expect(screen.getByRole("heading", { name: "Plugins" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Pi Extensions" })).toHaveAttribute("aria-selected", "true");
expect(await screen.findByTestId("pi-extensions-manager")).toBeInTheDocument();
});
it("shows direct merge commit routing only for direct merges", async () => { it("shows direct merge commit routing only for direct merges", async () => {
renderModal(); renderModal();
await waitForSettingsModalReady(); await waitForSettingsModalReady();
@@ -2416,7 +2451,7 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady(); await waitForSettingsModalReady();
expect(screen.queryByRole("button", { name: /Remote Access/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Remote Access/i })).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Authentication" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "General" })).toBeInTheDocument();
}); });
it("hides research settings nav items when experimentalFeatures.researchView is disabled", async () => { it("hides research settings nav items when experimentalFeatures.researchView is disabled", async () => {
@@ -2455,7 +2490,7 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady(); await waitForSettingsModalReady();
expect(screen.queryByRole("button", { name: /Research Defaults/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Research Defaults/i })).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Authentication" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "General" })).toBeInTheDocument();
}); });
it("hides scheduled evals nav item when experimentalFeatures.evalsView is disabled", async () => { it("hides scheduled evals nav item when experimentalFeatures.evalsView is disabled", async () => {
@@ -2492,7 +2527,7 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady(); await waitForSettingsModalReady();
expect(screen.queryByRole("button", { name: /Scheduled Evals/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Scheduled Evals/i })).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Authentication" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "General" })).toBeInTheDocument();
}); });
}); });
@@ -3631,7 +3666,7 @@ describe("SettingsModal", () => {
it("falls back to first visible section when initial section is unavailable", async () => { it("falls back to first visible section when initial section is unavailable", async () => {
renderModal({ initialSection: "unknown-section" as any }); renderModal({ initialSection: "unknown-section" as any });
await waitForSettingsModalReady(); await waitForSettingsModalReady();
expect(await screen.findByRole("heading", { name: "Authentication" })).toBeInTheDocument(); expect(await screen.findByRole("heading", { name: "General" })).toBeInTheDocument();
}); });
}); });