feat(backup): Export dropdown in Configuration header; warning is dismiss-only

Drop the Export button from the config-backup warning banner — it's now just the alert + dismiss (x). On the Configuration tab the top-right primary action becomes an 'Export' dropdown (first item: Repository Passwords, reusing the existing export-passwords action) so more export types can be added later. Other tabs keep Backup all apps / Add location. Menu opens from the trigger and closes on outside click, item click, or tab switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-23 00:14:06 +01:00
parent 5c6f4f4a2c
commit 4568ec51ef
3 changed files with 92 additions and 9 deletions

View File

@ -865,6 +865,50 @@
color: var(--text-primary);
}
/* Export dropdown in the backup page header (Configuration tab). */
#backup-page-header .page-header-actions {
position: relative;
}
.backup-export-menu {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 50;
min-width: 210px;
padding: 6px;
background: var(--card-bg);
border: 1px solid var(--card-border, var(--border-color));
border-radius: 10px;
box-shadow: var(--card-shadow, 0 8px 24px rgba(0, 0, 0, 0.25));
display: flex;
flex-direction: column;
gap: 2px;
}
.backup-export-menu[hidden] {
display: none;
}
.backup-export-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
padding: 8px 10px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-primary);
font-size: 0.85rem;
cursor: pointer;
}
.backup-export-menu-item:hover {
background: rgba(var(--text-rgb), 0.08);
}
.backup-warning-banner [data-action="export-passwords"] {
flex-shrink: 0;
white-space: nowrap;

View File

@ -55,6 +55,16 @@
Refresh
</button>
<button class="backup-primary-btn" id="backup-primary-action"></button>
<div class="backup-export-menu" id="backup-export-menu" role="menu" hidden>
<button type="button" class="backup-export-menu-item" data-action="export-passwords" role="menuitem">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Repository Passwords
</button>
</div>
</div>
</div>
<div class="backup-page-body">

View File

@ -136,6 +136,14 @@ class BackupPage {
// the URL via parseTabFromUrl() at init time.
document.addEventListener('click', (e) => {
// Clicking outside the export dropdown (and not on its trigger) closes it.
const exportMenu = document.getElementById('backup-export-menu');
if (exportMenu && !exportMenu.hidden
&& !e.target.closest('#backup-export-menu')
&& !e.target.closest('#backup-primary-action')) {
this.toggleExportMenu(false);
}
const tabBtn = e.target.closest('.backup-layout .sidebar .category[data-backup-tab]');
if (tabBtn) {
this.switchTab(tabBtn.dataset.backupTab);
@ -201,7 +209,7 @@ class BackupPage {
if (engineBtn) { this.openEngineDetailsModal(engineBtn); return; }
const exportBtn = e.target.closest('[data-action="export-passwords"]');
if (exportBtn) { this.exportRepositoryPasswords(exportBtn); return; }
if (exportBtn) { this.toggleExportMenu(false); this.exportRepositoryPasswords(exportBtn); return; }
const dismissWarn = e.target.closest('[data-action="dismiss-config-warning"]');
if (dismissWarn) {
@ -381,6 +389,8 @@ class BackupPage {
updatePrimaryAction() {
const btn = document.getElementById('backup-primary-action');
if (!btn) return;
// Switching tabs always closes the export dropdown.
this.toggleExportMenu(false);
if (this.currentTab === 'locations') {
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -390,6 +400,21 @@ class BackupPage {
Add location
`;
btn.dataset.intent = 'add-location';
btn.removeAttribute('aria-haspopup');
} else if (this.currentTab === 'configuration') {
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
`;
btn.dataset.intent = 'export-menu';
btn.setAttribute('aria-haspopup', 'menu');
} else {
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -400,6 +425,7 @@ class BackupPage {
Backup all apps
`;
btn.dataset.intent = 'backup-all';
btn.removeAttribute('aria-haspopup');
}
}
@ -407,11 +433,22 @@ class BackupPage {
const intent = document.getElementById('backup-primary-action')?.dataset.intent;
if (intent === 'add-location') {
this.openAddLocationModal();
} else if (intent === 'export-menu') {
this.toggleExportMenu();
} else {
this.runBackupAllApps();
}
}
toggleExportMenu(force) {
const menu = document.getElementById('backup-export-menu');
const btn = document.getElementById('backup-primary-action');
if (!menu) return;
const show = typeof force === 'boolean' ? force : menu.hidden;
menu.hidden = !show;
if (btn) btn.setAttribute('aria-expanded', show ? 'true' : 'false');
}
render() {
this.renderDashboard();
this.renderLocations();
@ -808,14 +845,6 @@ class BackupPage {
<strong>Keep your LibrePortal config backed up offline.</strong>
<span>Repository passwords live inside the config directory. Without that backup, snapshots cannot be decrypted by anyone including you.</span>
</div>
<button type="button" class="backup-secondary-btn" data-action="export-passwords">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export Repository Passwords
</button>
<button type="button" class="backup-warning-banner-close" data-action="dismiss-config-warning" title="Dismiss this warning" aria-label="Dismiss this warning">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>