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";
export type SectionId = SettingsSection["id"] | LegacySectionId;
const DEFAULT_SETTINGS_SECTION: SectionId = "global-general";
type PluginsSubsectionId = "fusion-plugins" | "pi-extensions";
/** Local form state extends Settings with a worktreeInitCommand override and lets tokenCap carry null (delete semantic). */
@@ -335,7 +337,7 @@ interface SettingsModalProps {
onClose: () => void;
addToast: (message: string, type?: ToastType) => void;
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;
/** Current theme mode */
themeMode?: ThemeMode;
@@ -414,13 +416,13 @@ export function SettingsModal({
const [scopedSettings, setScopedSettings] = useState<{ global: GlobalSettings; project: Partial<Settings> } | null>(null);
// Track initial scoped values for null-as-delete semantics on project overrides
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 [activeSection, setActiveSection] = useState<SectionId>(() => {
if (initialSection === "pi-extensions") {
return "plugins";
}
return initialSection ?? firstNonHeaderSection?.id ?? "authentication";
return initialSection ?? DEFAULT_SETTINGS_SECTION;
});
// Deterministic default: opening Plugins starts on Fusion Plugins unless legacy
// `initialSection="pi-extensions"` is explicitly provided.
@@ -470,7 +472,9 @@ export function SettingsModal({
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 */
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 type { ComponentProps } from "react";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SettingsModal } from "../SettingsModal";
@@ -196,11 +197,12 @@ const defaultSettings = {
webhookEvents: undefined,
};
function renderModal(props = {}) {
function renderModal(props: Partial<ComponentProps<typeof SettingsModal>> = {}) {
return render(
<SettingsModal
onClose={noop}
addToast={noop}
initialSection="authentication"
{...props}
/>
);
@@ -243,13 +245,46 @@ describe("SettingsModal", () => {
const authenticationHeading = screen.getByRole("heading", { name: "Authentication" });
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" });
expect(generalHeading).toHaveClass("settings-section-heading");
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 () => {
renderModal();
await waitForSettingsModalReady();
@@ -2416,7 +2451,7 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady();
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 () => {
@@ -2455,7 +2490,7 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady();
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 () => {
@@ -2492,7 +2527,7 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady();
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 () => {
renderModal({ initialSection: "unknown-section" as any });
await waitForSettingsModalReady();
expect(await screen.findByRole("heading", { name: "Authentication" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "General" })).toBeInTheDocument();
});
});