diff --git a/changelog.d/features/11286-essentials-sidebar-preset.md b/changelog.d/features/11286-essentials-sidebar-preset.md new file mode 100644 index 0000000000..f05152001a --- /dev/null +++ b/changelog.d/features/11286-essentials-sidebar-preset.md @@ -0,0 +1 @@ +- **feat(dashboard):** add an Essentials sidebar preset that shows only the beginner core path (Home → Endpoints → API Keys → Providers → Health → Settings) while keeping Advanced tools reachable via Command Palette search ([#11286](https://github.com/diegosouzapw/OmniRoute/pull/11286)) diff --git a/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx index e18611ff2c..af8583b1c1 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx @@ -521,6 +521,7 @@ export default function SidebarTab() { const presetLabels: Record = { all: getSettingsLabel("presetAll", "All"), + essentials: getSettingsLabel("presetEssentials", "Essentials"), minimal: getSettingsLabel("presetMinimal", "Minimal"), developer: getSettingsLabel("presetDeveloper", "Developer"), admin: getSettingsLabel("presetAdmin", "Admin"), @@ -528,6 +529,10 @@ export default function SidebarTab() { const presetDescriptions: Record = { all: getSettingsLabel("presetAllDesc", "Show everything"), + essentials: getSettingsLabel( + "presetEssentialsDesc", + "Beginner path — Advanced tools stay searchable" + ), minimal: getSettingsLabel("presetMinimalDesc", "Core pages only"), developer: getSettingsLabel("presetDeveloperDesc", "Dev & proxy tools"), admin: getSettingsLabel("presetAdminDesc", "Monitoring & audit"), diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 4aaaf250cc..6a6ec59863 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -6707,6 +6707,18 @@ "sidebarVisibility": "Hide sidebar items", "sidebarVisibilityDesc": "Hide any sidebar navigation entry to reduce visual clutter without disabling any features", "sidebarVisibilityHint": "Any sidebar section is hidden automatically when all of its entries are hidden", + "presetAll": "All", + "presetAllDesc": "Show everything", + "presetEssentials": "Essentials", + "presetEssentialsDesc": "Beginner path - Advanced tools stay searchable", + "presetMinimal": "Minimal", + "presetMinimalDesc": "Core pages only", + "presetDeveloper": "Developer", + "presetDeveloperDesc": "Dev & proxy tools", + "presetAdmin": "Admin", + "presetAdminDesc": "Monitoring & audit", + "settingsSidebarTitle": "Sidebar Customization", + "settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable.", "hideHealthLogs": "Hide Health Check Logs", "hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console", "themeAccent": "Theme color", diff --git a/src/shared/components/CommandPalette.tsx b/src/shared/components/CommandPalette.tsx index 0f4868b25c..096b57b287 100644 --- a/src/shared/components/CommandPalette.tsx +++ b/src/shared/components/CommandPalette.tsx @@ -6,8 +6,11 @@ import { useTranslations } from "next-intl"; import { SIDEBAR_SECTIONS, HIDDEN_SIDEBAR_ITEMS_SETTING_KEY, + SIDEBAR_PRESET_KEY, + ESSENTIALS_ADVANCED_TOOL_IDS, normalizeHiddenSidebarItems, resolveRuntimeSidebarSections, + type HideableSidebarItemId, type SidebarItemDefinition, type SidebarSectionChild, } from "@/shared/constants/sidebarVisibility"; @@ -61,6 +64,7 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { const [query, setQuery] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); const [hiddenItems, setHiddenItems] = useState>(new Set()); + const [activePreset, setActivePreset] = useState(null); const [radarAdminUrl, setRadarAdminUrl] = useState(null); useEffect(() => { @@ -71,6 +75,9 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { setHiddenItems( new Set(normalizeHiddenSidebarItems(data?.[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY])) ); + setActivePreset( + typeof data?.[SIDEBAR_PRESET_KEY] === "string" ? data[SIDEBAR_PRESET_KEY] : null + ); setRadarAdminUrl(data?.radarAdminUrl ?? null); }) .catch(() => { @@ -104,7 +111,13 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { if (isSidebarGroup(child)) { const subgroupLabel = safeTranslate(child.titleKey, child.titleFallback); return child.items - .filter((item) => !hiddenItems.has(item.id)) + .filter((item) => { + if (!hiddenItems.has(item.id)) return true; + return ( + activePreset === "essentials" && + ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId) + ); + }) .map((item) => ({ id: item.id, href: item.href, @@ -121,7 +134,12 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { })); } const item = child as SidebarItemDefinition; - if (hiddenItems.has(item.id)) return []; + if (hiddenItems.has(item.id)) { + const keepForEssentials = + activePreset === "essentials" && + ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId); + if (!keepForEssentials) return []; + } return [ { id: item.id, diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index 81e256038a..270715ef42 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -202,6 +202,36 @@ export const SIDEBAR_ITEM_ORDER_KEY = "sidebarItemOrder"; export const SIDEBAR_PRESET_KEY = "sidebarActivePreset"; export const SIDEBAR_SETTINGS_UPDATED_EVENT = "omniroute:settings-updated"; +/** Beginner Essentials: core path only. Advanced tools stay reachable via search. */ +const ESSENTIALS_SHOWN: ReadonlySet = new Set([ + "home", + "endpoints", + "api-manager", + "providers", + "health", + "settings-general", + "settings-sidebar", +]); + +/** Hidden in Essentials sidebar but kept searchable in Command Palette. */ +export const ESSENTIALS_ADVANCED_TOOL_IDS: ReadonlySet = new Set([ + "playground", + "logs", + "batch", + "translator", + "combos", + "quota", + "analytics", + "costs", + "cache", + "runtime", + "resilience-connections", + "mcp", + "a2a", + "memory", + "skills", +]); + const MINIMAL_SHOWN: ReadonlySet = new Set([ "home", "endpoints", @@ -297,6 +327,7 @@ function buildHiddenList(shown: ReadonlySet): HideableSid export const SIDEBAR_PRESETS: readonly SidebarPresetDefinition[] = [ { id: "all", icon: "select_all", hiddenItems: [] }, + { id: "essentials", icon: "star", hiddenItems: buildHiddenList(ESSENTIALS_SHOWN) }, { id: "minimal", icon: "minimize", hiddenItems: buildHiddenList(MINIMAL_SHOWN) }, { id: "developer", icon: "code", hiddenItems: buildHiddenList(DEVELOPER_SHOWN) }, { id: "admin", icon: "admin_panel_settings", hiddenItems: buildHiddenList(ADMIN_SHOWN) }, diff --git a/src/shared/constants/sidebarVisibility/types.ts b/src/shared/constants/sidebarVisibility/types.ts index 3bb6330ed0..159e6d0349 100644 --- a/src/shared/constants/sidebarVisibility/types.ts +++ b/src/shared/constants/sidebarVisibility/types.ts @@ -174,7 +174,7 @@ export interface SidebarSectionDefinition { defaultPinned?: boolean; } -export type SidebarPresetId = "all" | "minimal" | "developer" | "admin"; +export type SidebarPresetId = "all" | "essentials" | "minimal" | "developer" | "admin"; export interface SidebarPresetDefinition { id: SidebarPresetId; diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index beefb5fc8f..def9327741 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -199,7 +199,10 @@ export const updateSettingsSchema = z.object({ .array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]])) .optional(), sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(), - sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(), + sidebarActivePreset: z + .enum(["all", "essentials", "minimal", "developer", "admin"]) + .nullable() + .optional(), comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), codexServiceTier: z .object({ diff --git a/tests/unit/sidebar-customization.test.ts b/tests/unit/sidebar-customization.test.ts index 405265d71b..8c612bcd68 100644 --- a/tests/unit/sidebar-customization.test.ts +++ b/tests/unit/sidebar-customization.test.ts @@ -89,9 +89,10 @@ test("applyItemOrder ignores unknown IDs in order list", () => { // ─── SIDEBAR_PRESETS ────────────────────────────────────────────────────────── -test("SIDEBAR_PRESETS contains all four preset IDs", () => { +test("SIDEBAR_PRESETS contains all five preset IDs", () => { const ids = SIDEBAR_PRESETS.map((p) => p.id); assert.ok(ids.includes("all"), "expected 'all' preset"); + assert.ok(ids.includes("essentials"), "expected 'essentials' preset"); assert.ok(ids.includes("minimal"), "expected 'minimal' preset"); assert.ok(ids.includes("developer"), "expected 'developer' preset"); assert.ok(ids.includes("admin"), "expected 'admin' preset"); @@ -112,6 +113,30 @@ test("SIDEBAR_PRESETS 'all' preset has no hidden items", () => { assert.deepEqual(allPreset.hiddenItems, []); }); +test("SIDEBAR_PRESETS includes essentials as the beginner path", () => { + assert.equal(SIDEBAR_PRESETS.length, 5); + assert.deepEqual( + SIDEBAR_PRESETS.map((p) => p.id), + ["all", "essentials", "minimal", "developer", "admin"] + ); + const essentials = SIDEBAR_PRESETS.find((p) => p.id === "essentials"); + assert.ok(essentials, "expected 'essentials' preset to exist"); + const hidden = new Set(essentials.hiddenItems); + for (const id of [ + "home", + "endpoints", + "api-manager", + "providers", + "health", + "settings-general", + "settings-sidebar", + ]) { + assert.equal(hidden.has(id as never), false, `${id} should stay visible in essentials`); + } + assert.equal(hidden.has("playground"), true); + assert.equal(hidden.has("logs"), true); +}); + test("SIDEBAR_PRESETS non-all presets have at least one hidden item", () => { for (const preset of SIDEBAR_PRESETS.filter((p) => p.id !== "all")) { assert.ok(preset.hiddenItems.length > 0, `Preset '${preset.id}' should hide at least one item`); diff --git a/tests/unit/sidebar-essentials-static.test.ts b/tests/unit/sidebar-essentials-static.test.ts new file mode 100644 index 0000000000..f7da731f27 --- /dev/null +++ b/tests/unit/sidebar-essentials-static.test.ts @@ -0,0 +1,45 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +test("essentials preset is registered in sidebar visibility types and presets", () => { + const types = fs.readFileSync( + path.join(repoRoot, "src/shared/constants/sidebarVisibility/types.ts"), + "utf8" + ); + const visibility = fs.readFileSync( + path.join(repoRoot, "src/shared/constants/sidebarVisibility.ts"), + "utf8" + ); + const schema = fs.readFileSync( + path.join(repoRoot, "src/shared/validation/settingsSchemas.ts"), + "utf8" + ); + + assert.match(types, /"essentials"/); + assert.match(visibility, /id:\s*"essentials"/); + assert.match(visibility, /ESSENTIALS_ADVANCED_TOOL_IDS/); + assert.match(schema, /"essentials"/); +}); + +test("command palette keeps essentials advanced tools searchable", () => { + const source = fs.readFileSync( + path.join(repoRoot, "src/shared/components/CommandPalette.tsx"), + "utf8" + ); + assert.match(source, /ESSENTIALS_ADVANCED_TOOL_IDS/); + assert.match(source, /activePreset === "essentials"/); +}); + +test("essentials i18n keys exist in en.json", () => { + const en = JSON.parse( + fs.readFileSync(path.join(repoRoot, "src/i18n/messages/en.json"), "utf8") + ) as { settings: Record }; + assert.equal(en.settings.presetEssentials, "Essentials"); + assert.match(en.settings.presetEssentialsDesc, /Beginner path/i); + assert.match(en.settings.presetEssentialsDesc, /searchable/i); +});