refactor(navigation): remove legacy navigation.js and integrate logic into navigation_sidebar.js
This commit is contained in:
@@ -1,53 +0,0 @@
|
|||||||
// Navigation chevron buttons logic
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
const navPrev = document.getElementById("nav-prev");
|
|
||||||
const navNext = document.getElementById("nav-next");
|
|
||||||
|
|
||||||
if (!navPrev || !navNext) return;
|
|
||||||
|
|
||||||
// Define the navigation order (main pages)
|
|
||||||
const navPages = [
|
|
||||||
window.NAVIGATION_URLS.dashboard,
|
|
||||||
window.NAVIGATION_URLS.projects,
|
|
||||||
window.NAVIGATION_URLS.imports,
|
|
||||||
window.NAVIGATION_URLS.simulations,
|
|
||||||
window.NAVIGATION_URLS.reporting,
|
|
||||||
window.NAVIGATION_URLS.settings,
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
|
|
||||||
// Find current index
|
|
||||||
let currentIndex = -1;
|
|
||||||
for (let i = 0; i < navPages.length; i++) {
|
|
||||||
if (currentPath.startsWith(navPages[i])) {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found, disable both
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
navPrev.disabled = true;
|
|
||||||
navNext.disabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up prev button
|
|
||||||
if (currentIndex > 0) {
|
|
||||||
navPrev.addEventListener("click", function () {
|
|
||||||
window.location.href = navPages[currentIndex - 1];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
navPrev.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up next button
|
|
||||||
if (currentIndex < navPages.length - 1) {
|
|
||||||
navNext.addEventListener("click", function () {
|
|
||||||
window.location.href = navPages[currentIndex + 1];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
navNext.disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -3,6 +3,148 @@
|
|||||||
const SIDEBAR_SELECTOR = ".sidebar-nav";
|
const SIDEBAR_SELECTOR = ".sidebar-nav";
|
||||||
const DATA_SOURCE_ATTR = "navigationSource";
|
const DATA_SOURCE_ATTR = "navigationSource";
|
||||||
const ROLE_ATTR = "navigationRoles";
|
const ROLE_ATTR = "navigationRoles";
|
||||||
|
const NAV_PREV_ID = "nav-prev";
|
||||||
|
const NAV_NEXT_ID = "nav-next";
|
||||||
|
const CACHE_KEY = "calminer:navigation:sidebar";
|
||||||
|
const CACHE_VERSION = 1;
|
||||||
|
const CACHE_TTL_MS = 2 * 60 * 1000;
|
||||||
|
|
||||||
|
function hasStorage() {
|
||||||
|
try {
|
||||||
|
return typeof window.localStorage !== "undefined";
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCacheRoot() {
|
||||||
|
if (!hasStorage()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let raw;
|
||||||
|
try {
|
||||||
|
raw = window.localStorage.getItem(CACHE_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!raw) {
|
||||||
|
return { version: CACHE_VERSION, entries: {} };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (
|
||||||
|
!parsed ||
|
||||||
|
typeof parsed !== "object" ||
|
||||||
|
parsed.version !== CACHE_VERSION ||
|
||||||
|
typeof parsed.entries !== "object"
|
||||||
|
) {
|
||||||
|
return { version: CACHE_VERSION, entries: {} };
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch (error) {
|
||||||
|
clearCache();
|
||||||
|
return { version: CACHE_VERSION, entries: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCache(root) {
|
||||||
|
if (!hasStorage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(CACHE_KEY, JSON.stringify(root));
|
||||||
|
} catch (error) {
|
||||||
|
/* ignore storage write failures */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
if (!hasStorage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.localStorage.removeItem(CACHE_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normaliseRoles(roles) {
|
||||||
|
if (!Array.isArray(roles)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const seen = new Set();
|
||||||
|
const cleaned = [];
|
||||||
|
for (const value of roles) {
|
||||||
|
const role = typeof value === "string" ? value.trim() : "";
|
||||||
|
if (!role || seen.has(role)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(role);
|
||||||
|
cleaned.push(role);
|
||||||
|
}
|
||||||
|
cleaned.sort();
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serialiseRoles(roles) {
|
||||||
|
const cleaned = normaliseRoles(roles);
|
||||||
|
if (cleaned.length === 0) {
|
||||||
|
return "anonymous";
|
||||||
|
}
|
||||||
|
return cleaned.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentRoles(navContainer) {
|
||||||
|
const attr = navContainer.dataset[ROLE_ATTR];
|
||||||
|
if (!attr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const roles = attr
|
||||||
|
.split(",")
|
||||||
|
.map((role) => role.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (roles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCache(rolesKey) {
|
||||||
|
if (!rolesKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const root = loadCacheRoot();
|
||||||
|
if (!root || !root.entries || typeof root.entries !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entry = root.entries[rolesKey];
|
||||||
|
if (!entry || !entry.payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cachedAt = typeof entry.cachedAt === "number" ? entry.cachedAt : 0;
|
||||||
|
const expired = Date.now() - cachedAt > CACHE_TTL_MS;
|
||||||
|
return { payload: entry.payload, expired };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCache(rolesKey, payload) {
|
||||||
|
if (!rolesKey || !payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = loadCacheRoot();
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!root.entries || typeof root.entries !== "object") {
|
||||||
|
root.entries = {};
|
||||||
|
}
|
||||||
|
root.entries[rolesKey] = {
|
||||||
|
cachedAt: Date.now(),
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
root.version = CACHE_VERSION;
|
||||||
|
persistCache(root);
|
||||||
|
}
|
||||||
|
|
||||||
function onReady(callback) {
|
function onReady(callback) {
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
@@ -160,6 +302,102 @@
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePath(input) {
|
||||||
|
if (!input) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new URL(input, window.location.origin).pathname;
|
||||||
|
} catch (error) {
|
||||||
|
if (input.startsWith("/")) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return `/${input}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenNavigation(groups) {
|
||||||
|
const sequence = [];
|
||||||
|
for (const group of groups) {
|
||||||
|
if (!group || !Array.isArray(group.links)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const link of group.links) {
|
||||||
|
if (!link || !link.href) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const isExternal = Boolean(link.is_external ?? link.isExternal);
|
||||||
|
if (!isExternal) {
|
||||||
|
sequence.push({
|
||||||
|
href: link.href,
|
||||||
|
matchPrefix: link.match_prefix || link.matchPrefix || link.href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const children = Array.isArray(link.children) ? link.children : [];
|
||||||
|
for (const child of children) {
|
||||||
|
if (!child || !child.href) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const childExternal = Boolean(child.is_external ?? child.isExternal);
|
||||||
|
if (childExternal) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sequence.push({
|
||||||
|
href: child.href,
|
||||||
|
matchPrefix: child.match_prefix || child.matchPrefix || child.href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureChevronButtons(sequence) {
|
||||||
|
const prevButton = document.getElementById(NAV_PREV_ID);
|
||||||
|
const nextButton = document.getElementById(NAV_NEXT_ID);
|
||||||
|
if (!prevButton || !nextButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathname = window.location.pathname;
|
||||||
|
const normalised = sequence
|
||||||
|
.map((item) => ({
|
||||||
|
href: item.href,
|
||||||
|
matchPrefix: item.matchPrefix,
|
||||||
|
path: resolvePath(item.matchPrefix || item.href),
|
||||||
|
}))
|
||||||
|
.filter((item) => Boolean(item.path));
|
||||||
|
|
||||||
|
const currentIndex = normalised.findIndex((item) =>
|
||||||
|
isActivePath(pathname, item.matchPrefix || item.path)
|
||||||
|
);
|
||||||
|
|
||||||
|
prevButton.disabled = true;
|
||||||
|
prevButton.onclick = null;
|
||||||
|
nextButton.disabled = true;
|
||||||
|
nextButton.onclick = null;
|
||||||
|
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
const target = normalised[currentIndex - 1].href;
|
||||||
|
prevButton.disabled = false;
|
||||||
|
prevButton.onclick = () => {
|
||||||
|
window.location.href = target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < normalised.length - 1) {
|
||||||
|
const target = normalised[currentIndex + 1].href;
|
||||||
|
nextButton.disabled = false;
|
||||||
|
nextButton.onclick = () => {
|
||||||
|
window.location.href = target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderSidebar(navContainer, payload) {
|
function renderSidebar(navContainer, payload) {
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
const groups = Array.isArray(payload?.groups) ? payload.groups : [];
|
const groups = Array.isArray(payload?.groups) ? payload.groups : [];
|
||||||
@@ -177,6 +415,7 @@
|
|||||||
navContainer.appendChild(buildEmptyState());
|
navContainer.appendChild(buildEmptyState());
|
||||||
navContainer.dataset[DATA_SOURCE_ATTR] = "client-empty";
|
navContainer.dataset[DATA_SOURCE_ATTR] = "client-empty";
|
||||||
delete navContainer.dataset[ROLE_ATTR];
|
delete navContainer.dataset[ROLE_ATTR];
|
||||||
|
configureChevronButtons([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,9 +430,22 @@
|
|||||||
} else {
|
} else {
|
||||||
delete navContainer.dataset[ROLE_ATTR];
|
delete navContainer.dataset[ROLE_ATTR];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureChevronButtons(flattenNavigation(groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hydrateSidebar(navContainer) {
|
async function hydrateSidebar(navContainer) {
|
||||||
|
const roles = getCurrentRoles(navContainer);
|
||||||
|
const rolesKey = roles ? serialiseRoles(roles) : null;
|
||||||
|
const cached = readCache(rolesKey);
|
||||||
|
|
||||||
|
if (cached && cached.payload) {
|
||||||
|
renderSidebar(navContainer, cached.payload);
|
||||||
|
if (!cached.expired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(NAV_ENDPOINT, {
|
const response = await fetch(NAV_ENDPOINT, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -204,6 +456,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (!cached || !cached.payload) {
|
||||||
|
configureChevronButtons([]);
|
||||||
|
}
|
||||||
if (response.status !== 401 && response.status !== 403) {
|
if (response.status !== 401 && response.status !== 403) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Navigation sidebar hydration failed with status",
|
"Navigation sidebar hydration failed with status",
|
||||||
@@ -215,14 +470,22 @@
|
|||||||
|
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
renderSidebar(navContainer, payload);
|
renderSidebar(navContainer, payload);
|
||||||
|
const payloadRoles = Array.isArray(payload?.roles)
|
||||||
|
? payload.roles
|
||||||
|
: roles || [];
|
||||||
|
saveCache(serialiseRoles(payloadRoles), payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Navigation sidebar hydration failed", error);
|
console.warn("Navigation sidebar hydration failed", error);
|
||||||
|
if (!cached || !cached.payload) {
|
||||||
|
configureChevronButtons([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(() => {
|
onReady(() => {
|
||||||
const navContainer = document.querySelector(SIDEBAR_SELECTOR);
|
const navContainer = document.querySelector(SIDEBAR_SELECTOR);
|
||||||
if (!navContainer) {
|
if (!navContainer) {
|
||||||
|
configureChevronButtons([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hydrateSidebar(navContainer);
|
hydrateSidebar(navContainer);
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
<script src="/static/js/imports.js" defer></script>
|
<script src="/static/js/imports.js" defer></script>
|
||||||
<script src="/static/js/notifications.js" defer></script>
|
<script src="/static/js/notifications.js" defer></script>
|
||||||
<script src="/static/js/navigation_sidebar.js" defer></script>
|
<script src="/static/js/navigation_sidebar.js" defer></script>
|
||||||
<script src="/static/js/navigation.js" defer></script>
|
|
||||||
<script src="/static/js/theme.js"></script>
|
<script src="/static/js/theme.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {%
|
{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {% block head_extra %}
|
||||||
block head_extra %}
|
|
||||||
<link rel="stylesheet" href="/static/css/dashboard.css" />
|
<link rel="stylesheet" href="/static/css/dashboard.css" />
|
||||||
{% endblock %} {% block content %}
|
{% endblock %} {% block content %}
|
||||||
<section class="page-header dashboard-header">
|
<section class="page-header dashboard-header">
|
||||||
@@ -165,12 +164,12 @@ block head_extra %}
|
|||||||
</header>
|
</header>
|
||||||
<ul class="links-list">
|
<ul class="links-list">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/" target="_blank">CalMiner Repository</a>
|
<a href="https://git.allucanget.biz/allucanget/calminer" target="_blank">CalMiner Repository</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://example.com/docs" target="_blank">Documentation</a>
|
<a href="https://git.allucanget.biz/allucanget/calminer-docs" target="_blank">Documentation</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="mailto:support@example.com">Contact Support</a></li>
|
<li><a href="mailto:calminer@allucanget.biz">Contact Support</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% set sidebar_nav = get_sidebar_navigation(request) %}
|
{% set sidebar_nav = get_sidebar_navigation(request) %}
|
||||||
|
{% set nav_roles = sidebar_nav.roles if sidebar_nav and sidebar_nav.roles else [] %}
|
||||||
{% set nav_groups = sidebar_nav.groups if sidebar_nav else [] %}
|
{% set nav_groups = sidebar_nav.groups if sidebar_nav else [] %}
|
||||||
{% set current_path = request.url.path if request else '' %}
|
{% set current_path = request.url.path if request else '' %}
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@
|
|||||||
class="sidebar-nav"
|
class="sidebar-nav"
|
||||||
aria-label="Primary navigation"
|
aria-label="Primary navigation"
|
||||||
data-navigation-source="{{ 'server' if sidebar_nav else 'fallback' }}"
|
data-navigation-source="{{ 'server' if sidebar_nav else 'fallback' }}"
|
||||||
|
data-navigation-roles="{{ nav_roles | join(',') }}"
|
||||||
>
|
>
|
||||||
<div class="sidebar-nav-controls">
|
<div class="sidebar-nav-controls">
|
||||||
<button id="nav-prev" class="nav-chevron nav-chevron-prev" aria-label="Previous page">ᐊ</button>
|
<button id="nav-prev" class="nav-chevron nav-chevron-prev" aria-label="Previous page">ᐊ</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user