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:
5
.changeset/fn-4140-settings-default-general.md
Normal file
5
.changeset/fn-4140-settings-default-general.md
Normal 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.
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user