feat(branding): add hiddenNavHrefs to hide sidebar items via env var (#382)
Extends the branding service with a hiddenNavHrefs: string[] field so forks can hide sidebar entries (e.g. /salary, /customers) without patching DashboardNav.tsx. Configurable via NEXT_PUBLIC_BRANDING_HIDDEN_NAV as a comma-separated list. Default is [] — vanilla gnubok unchanged. Routes remain reachable; this is a nav-visibility switch only. Settings sidebar is intentionally out of scope. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
064fb7f7a9
commit
c86dbdc60d
@@ -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**.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,7 @@ registerBrandingService({
|
||||
// themeColor: '#000000',
|
||||
// manifestThemeColor: '#000000',
|
||||
// manifestBackgroundColor: '#ffffff',
|
||||
// hiddenNavHrefs: ['/salary', '/salary/employees', '/customers'],
|
||||
})
|
||||
|
||||
export const exampleBrandingExtension: Extension = {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<BrandingConfig> = {}
|
||||
@@ -88,5 +92,9 @@ function readEnvOverrides(): Partial<BrandingConfig> {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user