FN-7552: prefix Authentication with Global in mobile settings picker
Fixes the mobile Settings section picker showing bare "Authentication" instead of "Global — Authentication" for the storage-less (scope: undefined) Authentication section. - Add buildSettingsSectionGroupLabelMap to derive each section's owning group label (Global/Runtimes/Project) from SETTINGS_SECTIONS order - Extend resolveSettingsSectionOptionLabel to fall back to a Global-derived prefix for storage-less sections belonging to the Global group, without affecting sections that already declare a scope - Add regression test asserting the Authentication option renders "Global — Authentication" alongside existing scoped siblings - Add changeset (patch) documenting the fix Files changed: .changeset/fn-7552-mobile-authentication-global-prefix.md | 7 +++ packages/dashboard/app/components/SettingsModal.tsx | 40 +++++++++++++ packages/dashboard/app/components/__tests__/settings-mobile.test.tsx | 16 +++++ 3 files changed, 63 insertions(+) Fusion-Task-Id: FN-7552 Fusion-Task-Lineage: c389f556-c7d3-48a2-a51d-926a70bdaa0e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Show the "Global" prefix on the Authentication entry in the mobile Settings picker.
|
||||||
|
category: fix
|
||||||
|
dev: resolveSettingsSectionOptionLabel now derives the Global-group prefix for storage-less (scope: undefined) sections in SettingsModal.tsx (FN-7552).
|
||||||
@@ -292,6 +292,38 @@ function resolveFirstSelectableSettingsSection(sections: SettingsSection[], fall
|
|||||||
return sections.find((section) => !section.isGroupHeader)?.id ?? fallback;
|
return sections.find((section) => !section.isGroupHeader)?.id ?? fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:SettingsNavigation 2026-07-04-00:00:
|
||||||
|
The mobile Settings section picker (`<select>` on narrow viewports) prefixes every
|
||||||
|
section option with its owning group (`Global — `/`Project — `) so entries are
|
||||||
|
unambiguous when labels collide across scopes (e.g. "MCP Servers" exists in both
|
||||||
|
Global and Project). The Authentication section is intentionally `scope: undefined`
|
||||||
|
(it is not backed by settings storage — see SETTINGS_SECTIONS), but it still lives
|
||||||
|
under the Global group header in SETTINGS_SECTIONS, so its mobile option rendered as
|
||||||
|
bare "Authentication" instead of "Global — Authentication", inconsistent with its
|
||||||
|
Global-group siblings (FN-7552). SETTINGS_SECTION_GROUP_LABEL_BY_ID maps every
|
||||||
|
non-header section id to the label of the most recent group-header row preceding it
|
||||||
|
in SETTINGS_SECTIONS, so resolveSettingsSectionOptionLabel can fall back to a
|
||||||
|
group-derived "Global — " prefix for storage-less sections that belong to the Global
|
||||||
|
group — without changing behavior for any section that already declares a scope
|
||||||
|
(Runtimes entries keep their existing scope:"global" path) or for undefined-scope
|
||||||
|
group-header rows themselves (which are never rendered as selectable options).
|
||||||
|
*/
|
||||||
|
function buildSettingsSectionGroupLabelMap(sections: SettingsSection[]): Map<string, string> {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
let currentGroupLabel: string | undefined;
|
||||||
|
for (const section of sections) {
|
||||||
|
if (section.isGroupHeader) {
|
||||||
|
currentGroupLabel = section.label;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentGroupLabel !== undefined) {
|
||||||
|
map.set(section.id, currentGroupLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveSettingsSectionOptionLabel(section: SettingsSection, label: string): string {
|
function resolveSettingsSectionOptionLabel(section: SettingsSection, label: string): string {
|
||||||
if (section.scope === "global") {
|
if (section.scope === "global") {
|
||||||
return `Global — ${label}`;
|
return `Global — ${label}`;
|
||||||
@@ -299,6 +331,9 @@ function resolveSettingsSectionOptionLabel(section: SettingsSection, label: stri
|
|||||||
if (section.scope === "project") {
|
if (section.scope === "project") {
|
||||||
return `Project — ${label}`;
|
return `Project — ${label}`;
|
||||||
}
|
}
|
||||||
|
if (SETTINGS_SECTION_GROUP_LABEL_BY_ID.get(section.id) === "Global") {
|
||||||
|
return `Global — ${label}`;
|
||||||
|
}
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +419,11 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
|||||||
{ id: "plugins", label: "Plugins", labelKey: "settings.nav.plugins", scope: "project", searchableText: ["Fusion plugins", "Pi extensions", "plugin manager", "extension marketplace"] },
|
{ id: "plugins", label: "Plugins", labelKey: "settings.nav.plugins", scope: "project", searchableText: ["Fusion plugins", "Pi extensions", "plugin manager", "extension marketplace"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// FNXC:SettingsNavigation 2026-07-04-00:00: sectionId -> owning group label ("Global"/"Runtimes"/"Project"),
|
||||||
|
// derived once from SETTINGS_SECTIONS order. Used by resolveSettingsSectionOptionLabel to prefix
|
||||||
|
// storage-less (scope: undefined) sections like "authentication" that belong to the Global group (FN-7552).
|
||||||
|
const SETTINGS_SECTION_GROUP_LABEL_BY_ID = buildSettingsSectionGroupLabelMap(SETTINGS_SECTIONS);
|
||||||
|
|
||||||
/** Well-known experimental feature flags with display labels.
|
/** Well-known experimental feature flags with display labels.
|
||||||
* These always appear in the Experimental Features settings tab,
|
* These always appear in the Experimental Features settings tab,
|
||||||
* regardless of whether they exist in the project's settings blob.
|
* regardless of whether they exist in the project's settings blob.
|
||||||
|
|||||||
@@ -323,6 +323,22 @@ describe("SettingsModal mobile adaptations", () => {
|
|||||||
expect(getByText("No sections match this search.")).toBeTruthy();
|
expect(getByText("No sections match this search.")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FN-7552: the Authentication section is storage-less (scope: undefined) but belongs to the
|
||||||
|
// Global group in SETTINGS_SECTIONS, so its mobile picker option must still carry the
|
||||||
|
// "Global — " prefix like its Global-group siblings, without changing scoped sibling labels.
|
||||||
|
it("prefixes the storage-less Authentication section with 'Global —' in the mobile picker", async () => {
|
||||||
|
mockSettingsViewport(true);
|
||||||
|
const { getByLabelText } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
const picker = getByLabelText("Settings Section") as HTMLSelectElement;
|
||||||
|
const optionByValue = (value: string) => Array.from(picker.options).find((opt) => opt.value === value);
|
||||||
|
|
||||||
|
expect(optionByValue("authentication")?.textContent).toBe("Global — Authentication");
|
||||||
|
expect(optionByValue("global-mcp")?.textContent).toBe("Global — MCP Servers");
|
||||||
|
expect(optionByValue("mcp")?.textContent).toBe("Project — MCP Servers");
|
||||||
|
});
|
||||||
|
|
||||||
it("can open memory settings from the mobile section picker", async () => {
|
it("can open memory settings from the mobile section picker", async () => {
|
||||||
mockSettingsViewport(true);
|
mockSettingsViewport(true);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|||||||
Reference in New Issue
Block a user