Compare commits

..

No commits in common. "6f2d7ca0dced0377569ff76500e27bbb813771e2" and "92e585deba5e6088c79e3156c172b3f444fa8b3b" have entirely different histories.

6 changed files with 45 additions and 116 deletions

View File

@ -135,32 +135,22 @@ class AppTabbedManager {
return appName; return appName;
} }
// Get the main tab from the URL. Prefers the path-based shape // Get tab from URL parameter
// /app/<name>/<tab> (where <tab> defaults to "config" when absent)
// and falls back to the legacy `?tab=<tab>` query for older bookmarks /
// links that haven't been migrated yet. Legacy `logs` is aliased to `tasks`.
getTabFromURL() { getTabFromURL() {
if (window.appPartsFromPath) { const currentUrl = window.location.href;
const parts = window.appPartsFromPath(window.location.pathname);
if (parts.app) return parts.tab; // path wins on app pages
}
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const tab = urlParams.get('tab') || 'config'; const tab = urlParams.get('tab') || 'config';
// console.log('🔍 getTabFromURL debug:', {
//currentUrl: currentUrl,
//search: window.location.search,
//tabParam: urlParams.get('tab'),
//defaultTab: 'config',
//finalTab: tab === 'logs' ? 'tasks' : tab
//});
// Convert "logs" to "tasks" for backward compatibility
return tab === 'logs' ? 'tasks' : tab; return tab === 'logs' ? 'tasks' : tab;
} }
// Get the config sub-tab category from the URL. Only meaningful when the
// main tab is "config" — returns null otherwise (and when no sub-tab is set).
getConfigSubFromURL() {
if (window.appPartsFromPath) {
const parts = window.appPartsFromPath(window.location.pathname);
if (parts.tab === 'config' && parts.sub) return parts.sub;
}
// Legacy `?config=<category>` query support.
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('config') || null;
}
// Check if we're on an app page before doing anything // Check if we're on an app page before doing anything
isAppPage() { isAppPage() {
const pathname = window.location.pathname; const pathname = window.location.pathname;
@ -173,38 +163,34 @@ class AppTabbedManager {
search.includes('?=')); // Old format app pages search.includes('?=')); // Old format app pages
} }
// Update URL with app and tab — path-based: // Update URL with app and tab — path-based: /app/<app>?tab=<tab>&task=<task>.
// /app/<name> — config tab, no sub
// /app/<name>/<tab> — non-config main tab
// /app/<name>/config/<sub> — config tab + sub-category
// …?task=<id> — optional deep-link
updateURL(app = null, tab = null) { updateURL(app = null, tab = null) {
// Only update URLs on app pages - prevent interference with other pages. // Only update URLs on app pages - prevent interference with other pages.
if (!this.isAppPage()) return; if (!this.isAppPage()) return;
const currentParts = window.appPartsFromPath const params = new URLSearchParams(window.location.search);
? window.appPartsFromPath(window.location.pathname) const fromPath = window.location.pathname.replace(/^\/app\/?/, '').split('/')[0];
: { app: '', tab: 'config', sub: null }; const currentApp = app || (fromPath ? decodeURIComponent(fromPath) : '') || this.currentApp;
const currentApp = app || currentParts.app || this.currentApp;
if (!currentApp) return; if (!currentApp) return;
const finalTab = tab || currentParts.tab || 'config'; const q = new URLSearchParams();
// Keep the config sub-tab only if we're STAYING on config (and on the same const finalTab = tab || params.get('tab');
// app). Switching main tab or app drops it; staying on config preserves it. if (finalTab) q.set('tab', finalTab);
const finalSub = (!app && finalTab === 'config') ? currentParts.sub : null;
// Keep a deep-linked task only when staying on the same app (tab-only update). // Keep a deep-linked task only when staying on the same app (tab-only update).
const params = new URLSearchParams(window.location.search); if (!app) {
const taskId = !app ? params.get('task') : null; const task = params.get('task');
if (task) q.set('task', task);
}
const url = window.appPath(currentApp, finalTab, finalSub, taskId); const qs = q.toString();
window.history.replaceState({}, '', url); window.history.replaceState({}, '', `/app/${encodeURIComponent(currentApp)}${qs ? '?' + qs : ''}`);
} }
// Update current app and refresh content // Update current app and refresh content
updateApp(newAppName) { updateApp(newAppName) {
this.setCurrentApp(newAppName); this.setCurrentApp(newAppName);
// Reset to the config tab on the path-based app URL. // Reset to the config tab on the path-based app URL.
history.replaceState({}, '', window.appPath(newAppName, 'config')); history.replaceState({}, '', `/app/${encodeURIComponent(newAppName)}?tab=config`);
this.switchTab('config'); this.switchTab('config');
} }
@ -552,7 +538,10 @@ class AppTabbedManager {
// Get current app from AppTabbedManager // Get current app from AppTabbedManager
const currentApp = this.currentApp || ''; const currentApp = this.currentApp || '';
history.pushState({}, '', window.appPath(currentApp, 'tasks', null, taskId)); // Construct proper URL with correct parameter order
const newUrl = `/app/${currentApp}?tab=tasks&task=${taskId}`;
// console.log('🔍 Updating URL with task parameter:', newUrl);
history.pushState({}, '', newUrl);
} }
} else { } else {
console.warn('⚠️ App task details not found for:', taskId); console.warn('⚠️ App task details not found for:', taskId);

View File

@ -311,9 +311,10 @@ class AppsManager {
// console.log('🔍 Preserving existing tab:', targetTab); // console.log('🔍 Preserving existing tab:', targetTab);
} }
const newUrl = window.appPath(appName, targetTab); const newUrl = `/app/${appName}?tab=${targetTab}`;
// console.log('🔍 Setting URL to:', newUrl);
history.pushState({}, '', newUrl); history.pushState({}, '', newUrl);
// Update app-tabbed-manager BEFORE rendering the DOM. If renderAppDetail or // Update app-tabbed-manager BEFORE rendering the DOM. If renderAppDetail or
// any code it triggers calls switchTab → loadTabContent → restoreButtonState, // any code it triggers calls switchTab → loadTabContent → restoreButtonState,
// we need this.currentApp to already be updated so restoreButtonState checks // we need this.currentApp to already be updated so restoreButtonState checks
@ -368,7 +369,7 @@ class AppsManager {
} }
// Set URL to target tab (config or tasks) // Set URL to target tab (config or tasks)
const newUrl = window.appPath(appName, targetTab); const newUrl = `/app/${appName}?tab=${targetTab}`;
history.pushState({}, '', newUrl); history.pushState({}, '', newUrl);
// Update app-tabbed-manager. setCurrentApp clears stale disable state from // Update app-tabbed-manager. setCurrentApp clears stale disable state from
@ -576,12 +577,7 @@ class AppsManager {
// Use global preferred category if not provided // Use global preferred category if not provided
if (!preferredCategory && window.preferredConfigCategory) { if (!preferredCategory && window.preferredConfigCategory) {
preferredCategory = window.preferredConfigCategory; preferredCategory = window.preferredConfigCategory;
} // console.log('🎯 Using global preferred category:', preferredCategory);
// …or pick it out of the path (/app/<name>/config/<sub>) so a refresh /
// deep-link lands on the sub-tab encoded in the URL.
if (!preferredCategory && window.appPartsFromPath) {
const parts = window.appPartsFromPath(window.location.pathname);
if (parts.tab === 'config' && parts.sub) preferredCategory = parts.sub;
} }
const container = document.getElementById('app-detail-view'); const container = document.getElementById('app-detail-view');
@ -1872,33 +1868,22 @@ class AppsManager {
// Hide all panels // Hide all panels
const allPanels = document.querySelectorAll('.tab-panel'); const allPanels = document.querySelectorAll('.tab-panel');
allPanels.forEach(panel => panel.classList.remove('active')); allPanels.forEach(panel => panel.classList.remove('active'));
// Remove active from all config category tabs (not main navigation tabs) // Remove active from all config category tabs (not main navigation tabs)
const allButtons = document.querySelectorAll('.tab-panel:has(.config-section) .tab-button, .config-section .tab-button'); const allButtons = document.querySelectorAll('.tab-panel:has(.config-section) .tab-button, .config-section .tab-button');
allButtons.forEach(button => button.classList.remove('active')); allButtons.forEach(button => button.classList.remove('active'));
// Show selected panel // Show selected panel
const targetPanel = document.getElementById(`panel-${tabKey}`); const targetPanel = document.getElementById(`panel-${tabKey}`);
if (targetPanel) { if (targetPanel) {
targetPanel.classList.add('active'); targetPanel.classList.add('active');
} }
// Add active to clicked config category button // Add active to clicked config category button
const targetButton = document.querySelector(`.config-section [data-tab="${tabKey}"], .tab-panel:has(.config-section) [data-tab="${tabKey}"]`); const targetButton = document.querySelector(`.config-section [data-tab="${tabKey}"], .tab-panel:has(.config-section) [data-tab="${tabKey}"]`);
if (targetButton) { if (targetButton) {
targetButton.classList.add('active'); targetButton.classList.add('active');
} }
// Push the path-based URL so this sub-tab is shareable + back-buttonable —
// /app/<name>/config/<sub>. Skipped when there's no current app (e.g. when
// the form is rendered outside of the per-app context).
const currentApp = window.appTabbedManager?.currentApp;
if (currentApp && window.appPath) {
const newUrl = window.appPath(currentApp, 'config', tabKey);
if (window.location.pathname + window.location.search !== newUrl) {
history.pushState({}, '', newUrl);
}
}
} }
// Initialize simple tabs (working method from app-config-original.js) // Initialize simple tabs (working method from app-config-original.js)
@ -3242,10 +3227,10 @@ class AppsManager {
}, 500); }, 500);
} else if (window.librePortalSPA) { } else if (window.librePortalSPA) {
// Fallback: navigate to app with tasks tab // Fallback: navigate to app with tasks tab
const taskUrl = window.appPath(appName, 'tasks', null, task ? task.id : null); const taskUrl = task ? `/app/${appName}?tab=tasks&task=${task.id}` : `/app/${appName}?tab=tasks`;
window.librePortalSPA.navigateTo(taskUrl); window.librePortalSPA.navigateTo(taskUrl);
} else if (window.navigateToRoute) { } else if (window.navigateToRoute) {
window.navigateToRoute(window.appPath(appName, 'tasks', null, task ? task.id : null).replace(/^\//, '')); window.navigateToRoute(`app/${appName}?tab=tasks${task ? `&task=${task.id}` : ''}`);
} }
}, 1000); }, 1000);
@ -3487,11 +3472,11 @@ class AppsManager {
}, 500); }, 500);
} else if (window.librePortalSPA) { } else if (window.librePortalSPA) {
// Fallback: navigate to app with tasks tab // Fallback: navigate to app with tasks tab
const taskUrl = window.appPath(appName, 'tasks', null, task ? task.id : null); const taskUrl = task ? `/app/${appName}?tab=tasks&task=${task.id}` : `/app/${appName}?tab=tasks`;
// console.log(`🔄 Navigating to app tasks with uninstall task: ${task?.id}`); // console.log(`🔄 Navigating to app tasks with uninstall task: ${task?.id}`);
window.librePortalSPA.navigateTo(taskUrl); window.librePortalSPA.navigateTo(taskUrl);
} else if (window.navigateToRoute) { } else if (window.navigateToRoute) {
window.navigateToRoute(window.appPath(appName, 'tasks', null, task ? task.id : null).replace(/^\//, '')); window.navigateToRoute(`app/${appName}?tab=tasks${task ? `&task=${task.id}` : ''}`);
} }
}, 1000); }, 1000);

View File

@ -346,7 +346,7 @@ window.handleNotificationNavigation = (url) => {
// We're on an app page - navigate to the specified app and tab // We're on an app page - navigate to the specified app and tab
if (window.appTabbedManager) { if (window.appTabbedManager) {
// Update the URL to the target app/tab/task // Update the URL to the target app/tab/task
const newUrl = window.appPath(appName, tab, null, taskId); const newUrl = `/app/${appName}?tab=${tab}&task=${taskId}`;
console.log('🔗 Pushing state to URL:', newUrl); console.log('🔗 Pushing state to URL:', newUrl);
window.history.pushState({}, '', newUrl); window.history.pushState({}, '', newUrl);
@ -414,7 +414,7 @@ window.handleNotificationNavigation = (url) => {
} else { } else {
// Not on app or tasks page - navigate to the app's tasks tab // Not on app or tasks page - navigate to the app's tasks tab
if (appName && tab) { if (appName && tab) {
window.history.pushState({}, '', window.appPath(appName, tab, null, taskId)); window.history.pushState({}, '', `/app/${appName}?tab=${tab}&task=${taskId}`);
// Let the SPA handle the navigation // Let the SPA handle the navigation
if (window.appTabbedManager) { if (window.appTabbedManager) {
window.appTabbedManager.showAppDetail(appName); window.appTabbedManager.showAppDetail(appName);

View File

@ -296,7 +296,7 @@ async configUpdate(changes) {
const currentUrl = window.location.href; const currentUrl = window.location.href;
if (currentUrl.includes('/app/') && appName) { if (currentUrl.includes('/app/') && appName) {
taskUrl = window.appPath(appName, 'tasks', null, task.id); taskUrl = `/app/${appName}?tab=tasks&task=${task.id}`;
} else { } else {
taskUrl = `/tasks/all?task=${task.id}`; taskUrl = `/tasks/all?task=${task.id}`;
} }

View File

@ -1421,7 +1421,7 @@ class TasksManager {
: (appName || (task.command || `Task ${taskId}`))); : (appName || (task.command || `Task ${taskId}`)));
const onAppPage = window.location.pathname.startsWith('/app') && !window.location.pathname.startsWith('/apps'); const onAppPage = window.location.pathname.startsWith('/app') && !window.location.pathname.startsWith('/apps');
const url = (onAppPage && appName) const url = (onAppPage && appName)
? window.appPath(appName, 'tasks', null, taskId) ? `/app/${appName}?tab=tasks&task=${taskId}`
: `/tasks/all?task=${taskId}`; : `/tasks/all?task=${taskId}`;
const icon = isSystemTask const icon = isSystemTask
? '/icons/libreportal.svg' ? '/icons/libreportal.svg'

View File

@ -360,23 +360,7 @@ class LibrePortalSPAClean {
this.navigate('/apps', false); this.navigate('/apps', false);
return; return;
} }
// Back-compat: rewrite legacy /app/<name>?tab=<tab>[&config=<sub>][&task=<id>]
// URLs to the path-based shape (/app/<name>/<tab> | /<name>/config/<sub>)
// before the rest of the page reads URL state. The replaceState avoids
// leaving a stale query in the address bar for shareable links.
const legacyTab = url.searchParams.get('tab');
const legacyConfig = url.searchParams.get('config');
if (legacyTab || legacyConfig) {
const tab = legacyTab === 'logs' ? 'tasks' : (legacyTab || 'config');
const sub = (tab === 'config') ? legacyConfig : null;
const taskId = url.searchParams.get('task');
const canonical = window.appPath(appName, tab, sub, taskId);
if (canonical !== url.pathname + url.search) {
window.history.replaceState({}, '', canonical);
}
}
try { try {
const html = await this.fetchContent('/html/apps-unified-layout.html'); const html = await this.fetchContent('/html/apps-unified-layout.html');
this.loadContent(html, appName); // Will be updated after app data loads this.loadContent(html, appName); // Will be updated after app data loads
@ -555,35 +539,6 @@ window.adminCategoryFromPath = function (pathname) {
return segs[0]; return segs[0];
}; };
// App-detail path helpers — mirror adminPath but with one extra optional
// segment for the config sub-tab category. The shape is:
// /app/<name> — config tab, default sub-tab
// /app/<name>/<tab> — non-config main tab (tasks, backups, …)
// /app/<name>/config/<category> — config tab with a specific sub-tab
// plus an optional ?task=<id> query for deep-linking a single task on the
// tasks tab (a transient deep link, not part of the navigation hierarchy).
window.appPath = function (appName, tab, sub, taskId) {
if (!appName) return '/apps';
let p = '/app/' + encodeURIComponent(appName);
if (tab && tab !== 'config') {
p += '/' + encodeURIComponent(tab);
} else if (sub) {
p += '/config/' + encodeURIComponent(sub);
}
if (taskId) p += '?task=' + encodeURIComponent(taskId);
return p;
};
window.appPartsFromPath = function (pathname) {
const segs = String(pathname || '').replace(/^\/app\/?/, '').split('/').filter(Boolean);
const app = segs[0] ? decodeURIComponent(segs[0]) : '';
let tab = segs[1] || 'config';
let sub = null;
if (tab === 'config') sub = segs[2] ? decodeURIComponent(segs[2]) : null;
// `logs` is the legacy alias for the `tasks` main tab.
if (tab === 'logs') tab = 'tasks';
return { app, tab, sub };
};
// Global navigation function for click handlers // Global navigation function for click handlers
window.navigateToRoute = function(href) { window.navigateToRoute = function(href) {
if (window.spaClean) { if (window.spaClean) {