diff --git a/frontend/src/views/AppLayout.vue b/frontend/src/views/AppLayout.vue index ebf4a747..2d37253a 100644 --- a/frontend/src/views/AppLayout.vue +++ b/frontend/src/views/AppLayout.vue @@ -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) diff --git a/plugins/computers/plugin.py b/plugins/computers/plugin.py index 01bea5cd..babf4b9a 100644 --- a/plugins/computers/plugin.py +++ b/plugins/computers/plugin.py @@ -1653,6 +1653,7 @@ class ComputersPlugin(BasePlugin): 'icon': 'desktop', 'route': '/pcs', 'position': 15, + 'section': 'assets', }, ] diff --git a/plugins/geenforce/plugin.py b/plugins/geenforce/plugin.py index 7dc49e29..9db7fe47 100644 --- a/plugins/geenforce/plugin.py +++ b/plugins/geenforce/plugin.py @@ -125,6 +125,7 @@ class GeEnforcePlugin(BasePlugin): 'icon': 'shield', 'route': '/geenforce/manifests', 'position': 46, + 'section': 'fleet', }, ] diff --git a/plugins/guides/plugin.py b/plugins/guides/plugin.py index c8b0e387..13f30323 100644 --- a/plugins/guides/plugin.py +++ b/plugins/guides/plugin.py @@ -75,10 +75,15 @@ class GuidesPlugin(BasePlugin): ] def get_navigation_items(self) -> List[Dict]: - # Position 8, beside Slides (7), rather than down in the Information - # section with the reference lists. A guide is something you go and DO - # something with, like a slide deck you put on a screen - not a - # catalogue you look things up in. + # Position 8, directly under Slides (7), rather than down in the + # Information section with the reference lists. A guide is something you + # go and DO something with, like a slide deck you put on a screen - not + # a catalogue you look things up in. + # + # Warranties held 8 as well, and a TIE is broken by plugin load order, + # so Guides landed underneath it and read as "not under Slides at all". + # Warranties moved to 9. Two nav items sharing a position is not a + # preference, it is a coin toss. return [ { 'name': 'Guides', diff --git a/plugins/machines/plugin.py b/plugins/machines/plugin.py index 77258bf9..192469d0 100644 --- a/plugins/machines/plugin.py +++ b/plugins/machines/plugin.py @@ -232,6 +232,7 @@ class MachinesPlugin(BasePlugin): 'icon': 'cog', 'route': '/machines', 'position': 10, + 'section': 'assets', }, ] diff --git a/plugins/measuringtools/plugin.py b/plugins/measuringtools/plugin.py index 5a08e3fb..b563971f 100644 --- a/plugins/measuringtools/plugin.py +++ b/plugins/measuringtools/plugin.py @@ -74,6 +74,7 @@ class MeasuringToolsPlugin(BasePlugin): 'icon': 'ruler', 'route': '/measuringtools', 'position': 22, + 'section': 'assets', }, ] diff --git a/plugins/network/plugin.py b/plugins/network/plugin.py index 11e0837d..c88c291f 100644 --- a/plugins/network/plugin.py +++ b/plugins/network/plugin.py @@ -208,6 +208,7 @@ class NetworkPlugin(BasePlugin): 'icon': 'network-wired', 'route': '/network', 'position': 18, + 'section': 'assets', }, ] diff --git a/plugins/printedparts/plugin.py b/plugins/printedparts/plugin.py index 9e75d2db..d4896070 100644 --- a/plugins/printedparts/plugin.py +++ b/plugins/printedparts/plugin.py @@ -106,7 +106,8 @@ class PrintedpartsPlugin(BasePlugin): 'name': '3D Parts', 'icon': 'box', 'route': '/printedparts', - 'position': 46, + 'position': 27, + 'section': 'assets', }, ] diff --git a/plugins/printers/plugin.py b/plugins/printers/plugin.py index 058c8390..97164135 100644 --- a/plugins/printers/plugin.py +++ b/plugins/printers/plugin.py @@ -386,6 +386,7 @@ class PrintersPlugin(BasePlugin): 'icon': 'printer', 'route': '/printers', 'position': 20, + 'section': 'assets', }, ] diff --git a/plugins/tools/plugin.py b/plugins/tools/plugin.py index b86a564b..a305b417 100644 --- a/plugins/tools/plugin.py +++ b/plugins/tools/plugin.py @@ -121,6 +121,7 @@ class ToolsPlugin(BasePlugin): 'icon': 'wrench', 'route': '/tools', 'position': 47, + 'section': 'fleet', }, ] diff --git a/plugins/usb/plugin.py b/plugins/usb/plugin.py index bb6572cb..6164b813 100644 --- a/plugins/usb/plugin.py +++ b/plugins/usb/plugin.py @@ -70,6 +70,27 @@ class USBPlugin(BasePlugin): 'docs': 'plugins/usb/README.md', } + def get_settings_cards(self) -> List[Dict]: + """Somewhere to actually turn the kiosk on. + + The kiosk page tells an administrator to enable it in Settings under + USB. Without this card that place does not exist, and the instruction + is a dead end - the same trap the backups plugin shipped with, where + settings existed but only a shell could reach them. + """ + return [ + { + 'group': 'USB Devices', + 'to': '/settings/usb', + 'icon': 'usb', + 'title': 'USB Settings', + 'description': 'Logged-out kiosk for taking and returning ' + 'drives, and whether somebody may return a drive ' + 'checked out to another person', + 'position': 45, + }, + ] + def get_settings_defaults(self) -> List[Dict]: """Kiosk settings. @@ -144,7 +165,8 @@ class USBPlugin(BasePlugin): 'name': 'USB Devices', 'icon': 'usb', 'route': '/usb', - 'position': 45, + 'position': 25, + 'section': 'assets', }, ] diff --git a/plugins/warranty/plugin.py b/plugins/warranty/plugin.py index 3c7b3416..1d09ffcf 100644 --- a/plugins/warranty/plugin.py +++ b/plugins/warranty/plugin.py @@ -57,7 +57,7 @@ class WarrantyPlugin(BasePlugin): 'name': 'Warranties', 'icon': 'shield', 'route': '/warranties', - 'position': 8, + 'position': 9, }, ]