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:
parent
5c6f4f4a2c
commit
4568ec51ef
@ -865,6 +865,50 @@
|
|||||||
color: var(--text-primary);
|
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"] {
|
.backup-warning-banner [data-action="export-passwords"] {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@ -55,6 +55,16 @@
|
|||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
<button class="backup-primary-btn" id="backup-primary-action"></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>
|
</div>
|
||||||
<div class="backup-page-body">
|
<div class="backup-page-body">
|
||||||
|
|||||||
@ -136,6 +136,14 @@ class BackupPage {
|
|||||||
// the URL via parseTabFromUrl() at init time.
|
// the URL via parseTabFromUrl() at init time.
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
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]');
|
const tabBtn = e.target.closest('.backup-layout .sidebar .category[data-backup-tab]');
|
||||||
if (tabBtn) {
|
if (tabBtn) {
|
||||||
this.switchTab(tabBtn.dataset.backupTab);
|
this.switchTab(tabBtn.dataset.backupTab);
|
||||||
@ -201,7 +209,7 @@ class BackupPage {
|
|||||||
if (engineBtn) { this.openEngineDetailsModal(engineBtn); return; }
|
if (engineBtn) { this.openEngineDetailsModal(engineBtn); return; }
|
||||||
|
|
||||||
const exportBtn = e.target.closest('[data-action="export-passwords"]');
|
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"]');
|
const dismissWarn = e.target.closest('[data-action="dismiss-config-warning"]');
|
||||||
if (dismissWarn) {
|
if (dismissWarn) {
|
||||||
@ -381,6 +389,8 @@ class BackupPage {
|
|||||||
updatePrimaryAction() {
|
updatePrimaryAction() {
|
||||||
const btn = document.getElementById('backup-primary-action');
|
const btn = document.getElementById('backup-primary-action');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
|
// Switching tabs always closes the export dropdown.
|
||||||
|
this.toggleExportMenu(false);
|
||||||
if (this.currentTab === 'locations') {
|
if (this.currentTab === 'locations') {
|
||||||
btn.innerHTML = `
|
btn.innerHTML = `
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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
|
Add location
|
||||||
`;
|
`;
|
||||||
btn.dataset.intent = '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 {
|
} else {
|
||||||
btn.innerHTML = `
|
btn.innerHTML = `
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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
|
Backup all apps
|
||||||
`;
|
`;
|
||||||
btn.dataset.intent = 'backup-all';
|
btn.dataset.intent = 'backup-all';
|
||||||
|
btn.removeAttribute('aria-haspopup');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,11 +433,22 @@ class BackupPage {
|
|||||||
const intent = document.getElementById('backup-primary-action')?.dataset.intent;
|
const intent = document.getElementById('backup-primary-action')?.dataset.intent;
|
||||||
if (intent === 'add-location') {
|
if (intent === 'add-location') {
|
||||||
this.openAddLocationModal();
|
this.openAddLocationModal();
|
||||||
|
} else if (intent === 'export-menu') {
|
||||||
|
this.toggleExportMenu();
|
||||||
} else {
|
} else {
|
||||||
this.runBackupAllApps();
|
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() {
|
render() {
|
||||||
this.renderDashboard();
|
this.renderDashboard();
|
||||||
this.renderLocations();
|
this.renderLocations();
|
||||||
@ -808,14 +845,6 @@ class BackupPage {
|
|||||||
<strong>Keep your LibrePortal config backed up offline.</strong>
|
<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>
|
<span>Repository passwords live inside the config directory. Without that backup, snapshots cannot be decrypted by anyone — including you.</span>
|
||||||
</div>
|
</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">
|
<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">
|
<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>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user