Sidebar sections are DECLARED, not guessed from a position number

The section headings were derived from position ranges: 10-29 became Assets,
30-49 became Information. A number is for ORDER; it was never a statement about
meaning, and using it as one meant a plugin landed in whichever bucket its
number happened to fall in.

That is how USB Devices - an asset - came to sit under Information beside
GE-Enforce and Tech Tools, and 3D Parts with it. Nobody chose that. It also
meant two plugins picking the same number silently reordered each other:
Warranties and Guides both held 8, the tie broke on plugin load order, and
Guides rendered under Warranties rather than under Slides where it was asked to
go. Warranties moves to 9.

Each nav item now names its section and the renderer groups by what was
declared. An item with no section stays in the unlabelled group at the top,
which is the safe default: it appears, it is simply not filed.

Assets gains USB Devices and 3D Parts. GE-Enforce and Tech Tools move to a new
Fleet section - they are neither assets nor reference material, and calling them
Information was only ever an artefact of their position numbers.

Also: sorting used `position || 99`, and 0 is falsy in JavaScript, so Dashboard -
the only item that uses 0 - sorted to the end of its own group.
This commit is contained in:
cproudlock
2026-08-27 09:25:27 -04:00
parent ab5b48e3f4
commit 2ad2bd1b90
12 changed files with 91 additions and 35 deletions

View File

@@ -189,14 +189,33 @@ const defaultNav = [
{ name: 'Notifications', icon: 'bell', route: '/notifications', position: 5 },
{ name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6 },
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
{ name: 'Machines', icon: 'cog', route: '/machines', position: 10 },
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
{ name: 'USB Devices', icon: 'usb', route: '/usb', position: 45 },
{ name: 'Machines', icon: 'cog', route: '/machines', position: 10, section: 'assets' },
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15, section: 'assets' },
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18, section: 'assets' },
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20, section: 'assets' },
{ name: 'USB Devices', icon: 'usb', route: '/usb', position: 25, section: 'assets' },
{ name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information' },
{ name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35 },
{ name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40 },
{ name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35, section: 'information' },
{ name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40, section: 'information' },
]
// The canonical sidebar sections, in the order they render. A nav item DECLARES
// which one it belongs to; nothing is inferred.
//
// Sections used to be derived from the position number - 10-29 became Assets,
// 30-49 became Information - so a plugin landed in whichever bucket its number
// happened to fall in. That is how USB Devices, an asset, came to sit under
// Information next to GE-Enforce and Tech Tools, and why two plugins picking
// the same number silently reordered each other. A number is for ORDER; it was
// never a statement about meaning.
//
// An item with no section (or an unknown one) stays in the unlabelled group at
// the top, which is the safe default: it appears, it is just not filed.
const NAV_SECTIONS = [
{ key: null, label: null },
{ key: 'assets', label: 'Assets' },
{ key: 'information', label: 'Information' },
{ key: 'fleet', label: 'Fleet' },
]
function buildNavItems(items) {
@@ -206,29 +225,26 @@ function buildNavItems(items) {
const visible = items.filter(item =>
!item.permission || authStore.hasPermission(item.permission))
// Sort by position
const sorted = [...visible].sort((a, b) => (a.position || 99) - (b.position || 99))
const known = new Set(NAV_SECTIONS.map(s => s.key).filter(Boolean))
const bucket = item => {
const declared = (item.section || '').toLowerCase()
return known.has(declared) ? declared : null
}
// Assign section headers based on position ranges
const result = []
let currentSection = null
for (const item of sorted) {
let section = null
if (item.position >= 10 && item.position < 30 && currentSection !== 'Assets') {
section = 'Assets'
currentSection = 'Assets'
} else if (item.section === 'information' || (item.position >= 30 && item.position < 50 && currentSection !== 'Information')) {
if (currentSection !== 'Information') {
section = 'Information'
currentSection = 'Information'
}
}
result.push({
...item,
section,
iconComponent: iconMap[item.icon] || null
for (const { key, label } of NAV_SECTIONS) {
const inSection = visible
.filter(item => bucket(item) === key)
// ?? not ||: position 0 is a real position, and `0 || 99` is 99, which
// sent Dashboard - the only item that uses 0 - to the end of its group.
.sort((a, b) => (a.position ?? 99) - (b.position ?? 99))
inSection.forEach((item, index) => {
result.push({
...item,
// Only the first item in a section carries its heading.
section: index === 0 ? label : null,
iconComponent: iconMap[item.icon] || null,
})
})
}
@@ -236,6 +252,11 @@ function buildNavItems(items) {
}
onMounted(async () => {
// Refresh the signed-in user (and their permissions) on every app load. The
// login payload is cached in localStorage, so without this a permission
// granted mid-session stays invisible until the person logs out and back in.
if (authStore.isAuthenticated) authStore.fetchUser()
// Half-hourly: cheap, and it crosses midnight into the new week without a
// reload on a screen nobody touches.
fiscalWeekTimer = setInterval(() => { currentFiscalWeek.value = fiscalWeek() }, 30 * 60 * 1000)