diff --git a/WHITELABEL.md b/WHITELABEL.md index ad3fc477..6d660d88 100644 --- a/WHITELABEL.md +++ b/WHITELABEL.md @@ -47,6 +47,7 @@ All branding can be set via env vars. Public ones use `NEXT_PUBLIC_BRANDING_*` ( | `NEXT_PUBLIC_BRANDING_THEME_COLOR` | `themeColor` | `#304D83` | | `NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR` | `manifestThemeColor` | `#1a1a1a` | | `NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR` | `manifestBackgroundColor` | `#ffffff` | +| `NEXT_PUBLIC_BRANDING_HIDDEN_NAV` | `hiddenNavHrefs` (comma-separated, e.g. `/salary,/customers`) | `` (none hidden) | Resolution order (last wins): **defaults → env vars → extension override**. diff --git a/components/dashboard/DashboardNav.tsx b/components/dashboard/DashboardNav.tsx index dbe481f6..71989c82 100644 --- a/components/dashboard/DashboardNav.tsx +++ b/components/dashboard/DashboardNav.tsx @@ -29,6 +29,7 @@ import { HandCoins, Sparkles, } from 'lucide-react' +import { getBranding } from '@/lib/branding/service' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { isAgentInboxEnabled } from '@/lib/ai/feature-flag' import { resolveIcon } from '@/lib/extensions/icon-resolver' @@ -157,9 +158,12 @@ export default function DashboardNav({ companyName: _companyName, entityType, un }, 200) } + const hiddenNavHrefs = new Set(getBranding().hiddenNavHrefs) + // Filter nav items by entity type, hidden flag, and conditional visibility const filteredItems = navItems.filter(item => { if (item.hidden) return false + if (hiddenNavHrefs.has(item.href)) return false if (item.modes && !item.modes.includes(entityType)) return false // Only show Granskning when there are pending operations if (item.href === '/pending' && pendingOperationsCount === 0) return false diff --git a/extensions/general/_example-branding/index.ts b/extensions/general/_example-branding/index.ts index 194f66b2..291265be 100644 --- a/extensions/general/_example-branding/index.ts +++ b/extensions/general/_example-branding/index.ts @@ -19,6 +19,7 @@ registerBrandingService({ // themeColor: '#000000', // manifestThemeColor: '#000000', // manifestBackgroundColor: '#ffffff', + // hiddenNavHrefs: ['/salary', '/salary/employees', '/customers'], }) export const exampleBrandingExtension: Extension = { diff --git a/lib/branding/__tests__/service.test.ts b/lib/branding/__tests__/service.test.ts index ee8f65e5..cd19087b 100644 --- a/lib/branding/__tests__/service.test.ts +++ b/lib/branding/__tests__/service.test.ts @@ -14,6 +14,7 @@ const ENV_KEYS = [ 'NEXT_PUBLIC_BRANDING_THEME_COLOR', 'NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR', 'NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR', + 'NEXT_PUBLIC_BRANDING_HIDDEN_NAV', ] as const describe('branding service', () => { @@ -51,6 +52,32 @@ describe('branding service', () => { expect(b.themeColor).toBe('#304D83') expect(b.manifestThemeColor).toBe('#1a1a1a') expect(b.manifestBackgroundColor).toBe('#ffffff') + expect(b.hiddenNavHrefs).toEqual([]) + }) + + it('parses NEXT_PUBLIC_BRANDING_HIDDEN_NAV as comma-separated hrefs', async () => { + process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = '/salary,/salary/employees,/customers' + const { getBranding } = await import('../service') + expect(getBranding().hiddenNavHrefs).toEqual(['/salary', '/salary/employees', '/customers']) + }) + + it('trims whitespace and drops empty entries in hidden nav list', async () => { + process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = ' /salary , ,/customers, ' + const { getBranding } = await import('../service') + expect(getBranding().hiddenNavHrefs).toEqual(['/salary', '/customers']) + }) + + it('empty NEXT_PUBLIC_BRANDING_HIDDEN_NAV keeps default empty list', async () => { + process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = '' + const { getBranding } = await import('../service') + expect(getBranding().hiddenNavHrefs).toEqual([]) + }) + + it('extension override replaces hiddenNavHrefs', async () => { + process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = '/salary' + const { getBranding, registerBrandingService } = await import('../service') + registerBrandingService({ hiddenNavHrefs: ['/customers', '/suppliers'] }) + expect(getBranding().hiddenNavHrefs).toEqual(['/customers', '/suppliers']) }) it('env vars override defaults', async () => { diff --git a/lib/branding/service.ts b/lib/branding/service.ts index 52e1e6f7..a7ba81de 100644 --- a/lib/branding/service.ts +++ b/lib/branding/service.ts @@ -38,6 +38,9 @@ export interface BrandingConfig { themeColor: string manifestThemeColor: string manifestBackgroundColor: string + + // Navigation + hiddenNavHrefs: string[] } const DEFAULT_BRANDING: BrandingConfig = { @@ -55,6 +58,7 @@ const DEFAULT_BRANDING: BrandingConfig = { themeColor: '#304D83', manifestThemeColor: '#1a1a1a', manifestBackgroundColor: '#ffffff', + hiddenNavHrefs: [], } let _override: Partial = {} @@ -88,5 +92,9 @@ function readEnvOverrides(): Partial { if (env.NEXT_PUBLIC_BRANDING_THEME_COLOR) o.themeColor = env.NEXT_PUBLIC_BRANDING_THEME_COLOR if (env.NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR) o.manifestThemeColor = env.NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR if (env.NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR) o.manifestBackgroundColor = env.NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR + if (env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV) { + const hrefs = env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV.split(',').map(s => s.trim()).filter(Boolean) + if (hrefs.length > 0) o.hiddenNavHrefs = hrefs + } return o }