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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user