Compare commits

..

No commits in common. "8a99ebd080f594c7e5fc7809c6f02021000fd777" and "4b8f2c698cc3524bb869bcf79c8062423f303f9f" have entirely different histories.

2 changed files with 114 additions and 126 deletions

View File

@ -723,48 +723,47 @@
border-top: 1px dashed rgba(var(--text-rgb), 0.08); border-top: 1px dashed rgba(var(--text-rgb), 0.08);
} }
/* Tabbed location editor (Connection | Retention | Advanced). Splits the /* Full-width "Advanced" disclosure in the location editor's Connection
formerly long single form into one panel per concern. */ section. Reveals the advanced fields (URI override, SSH port, append-only). */
.backup-loc-tabs { .backup-loc-advanced-toggle {
display: flex; display: flex;
gap: 4px; align-items: center;
border-bottom: 1px solid rgba(var(--text-rgb), 0.10); gap: 8px;
margin-bottom: 16px; width: 100%;
} margin-top: 14px;
padding: 10px 12px;
.backup-loc-tab { background: rgba(var(--text-rgb), 0.03);
appearance: none; border: 1px solid rgba(var(--text-rgb), 0.08);
background: transparent; border-radius: 8px;
border: none; color: var(--text-primary);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
padding: 8px 14px;
color: rgba(var(--text-rgb), 0.6);
font: inherit; font: inherit;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: color 0.15s ease, border-color 0.15s ease; transition: background 0.15s ease;
} }
.backup-loc-tab:hover { .backup-loc-advanced-toggle:hover {
color: var(--text-primary); background: rgba(var(--text-rgb), 0.06);
} }
.backup-loc-tab.active { .backup-loc-advanced-chevron {
color: var(--accent); transition: transform 0.15s ease;
border-bottom-color: var(--accent); flex-shrink: 0;
} }
.backup-loc-tab-panel[hidden] { .backup-loc-advanced-toggle.open .backup-loc-advanced-chevron {
transform: rotate(90deg);
}
.backup-loc-advanced {
margin-top: 12px;
}
.backup-loc-advanced[hidden] {
display: none; display: none;
} }
.backup-loc-tab-panel > .category-description {
margin-top: 0;
margin-bottom: 12px;
}
.backup-form-footer { .backup-form-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -62,11 +62,6 @@ const BACKUP_LOC_FIELDS_BY_TYPE = {
rclone: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'] rclone: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY']
}; };
// Suffixes that live in the editor's "Advanced" tab. configs.json can flag
// more via a **ADVANCED** comment marker; this set keeps the known overrides
// advanced even on legacy location.configs that predate the marker.
const LOC_ADVANCED_SUFFIXES = new Set(['URI', 'SSH_PORT', 'APPEND_ONLY']);
function backupRetentionDetectPreset(values, includeInherit = false) { function backupRetentionDetectPreset(values, includeInherit = false) {
const norm = (v) => (v == null ? '' : String(v).trim()); const norm = (v) => (v == null ? '' : String(v).trim());
for (const [key, p] of Object.entries(BACKUP_RETENTION_PRESETS)) { for (const [key, p] of Object.entries(BACKUP_RETENTION_PRESETS)) {
@ -203,19 +198,15 @@ class BackupPage {
return; return;
} }
const locTab = e.target.closest('[data-action="loc-tab"]'); const advToggle = e.target.closest('[data-action="toggle-loc-advanced"]');
if (locTab) { if (advToggle) {
const tabIdx = locTab.dataset.loc; const sec = document.getElementById(advToggle.dataset.target);
const tabName = locTab.dataset.tab; if (sec) {
const root = locTab.closest('.backup-location-config') || document; const show = sec.hasAttribute('hidden');
root.querySelectorAll(`[data-action="loc-tab"][data-loc="${tabIdx}"]`).forEach(b => { sec.toggleAttribute('hidden', !show);
const on = b === locTab; advToggle.setAttribute('aria-expanded', show ? 'true' : 'false');
b.classList.toggle('active', on); advToggle.classList.toggle('open', show);
b.setAttribute('aria-selected', on ? 'true' : 'false'); }
});
root.querySelectorAll(`[data-tab-panel][data-loc="${tabIdx}"]`).forEach(p => {
p.toggleAttribute('hidden', p.dataset.tabPanel !== tabName);
});
return; return;
} }
@ -668,7 +659,7 @@ class BackupPage {
renderLocationDetailsBody(l) { renderLocationDetailsBody(l) {
const idx = l.idx; const idx = l.idx;
const groups = this.locFieldGroups(idx, l.type); const connectionFields = BACKUP_LOC_FIELDS_BY_TYPE[l.type] || BACKUP_LOC_FIELDS_BY_TYPE.local;
const retentionValues = { const retentionValues = {
last: l.custom_retention ? (l.keep_last || '') : '', last: l.custom_retention ? (l.keep_last || '') : '',
daily: l.custom_retention ? (l.keep_daily || '') : '', daily: l.custom_retention ? (l.keep_daily || '') : '',
@ -677,37 +668,34 @@ class BackupPage {
yearly: l.custom_retention ? (l.keep_yearly || '') : '' yearly: l.custom_retention ? (l.keep_yearly || '') : ''
}; };
const tab = (id, label) => `
<button type="button" class="backup-loc-tab${id === 'connection' ? ' active' : ''}" data-action="loc-tab" data-loc="${idx}" data-tab="${id}" role="tab" aria-selected="${id === 'connection'}">${label}</button>`;
const advancedBody = groups.advanced.length
? this.renderLocFields(idx, groups.advanced, l)
: `<div class="backup-empty-state">No advanced options for this location type.</div>`;
return ` return `
<div class="config-category backup-location-config" data-section="location-${idx}"> <div class="config-category backup-location-config" data-section="location-${idx}">
<div class="backup-loc-tabs" role="tablist"> <div class="domains-wrapper">
${tab('connection', 'Connection')} <div class="domains-header">
${tab('retention', 'Retention')} <div>
${tab('advanced', 'Advanced')} <h3>Connection</h3>
</div>
<div class="backup-loc-tab-panel" data-tab-panel="connection" data-loc="${idx}">
<p class="category-description">How LibrePortal connects to this storage location.</p> <p class="category-description">How LibrePortal connects to this storage location.</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="backup-location-connection-fields" id="backup-location-${idx}-connection"> <div class="backup-location-connection-fields" id="backup-location-${idx}-connection">
${this.renderLocFields(idx, groups.connection, l)} ${this.renderLocFields(idx, connectionFields, l)}
</div> </div>
</div> </div>
<div class="backup-loc-tab-panel" data-tab-panel="retention" data-loc="${idx}" hidden> </div>
<div class="config-category backup-location-config">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>Retention</h3>
<p class="category-description">When to delete old backups from this location.</p> <p class="category-description">When to delete old backups from this location.</p>
</div>
</div>
<div class="domains-divider"></div>
<div id="backup-location-${idx}-retention"> <div id="backup-location-${idx}-retention">
${this.formRetention(`CFG_BACKUP_LOC_${idx}_`, retentionValues, true)} ${this.formRetention(`CFG_BACKUP_LOC_${idx}_`, retentionValues, true)}
</div> </div>
</div> </div>
<div class="backup-loc-tab-panel" data-tab-panel="advanced" data-loc="${idx}" hidden>
<p class="category-description">Overrides most locations don't need.</p>
<div id="backup-location-${idx}-advanced">
${advancedBody}
</div>
</div>
</div> </div>
<div class="backup-location-actions"> <div class="backup-location-actions">
<button class="backup-primary-btn" data-action="save-location" data-loc="${idx}"> <button class="backup-primary-btn" data-action="save-location" data-loc="${idx}">
@ -762,27 +750,15 @@ class BackupPage {
} }
refreshInlineTypeFields(idx, type) { refreshInlineTypeFields(idx, type) {
const container = document.getElementById(`backup-location-${idx}-connection`);
if (!container) return;
const loc = (this.locations?.locations || []).find(l => l.idx === idx) || {}; const loc = (this.locations?.locations || []).find(l => l.idx === idx) || {};
const groups = this.locFieldGroups(idx, type); const suffixes = BACKUP_LOC_FIELDS_BY_TYPE[type] || BACKUP_LOC_FIELDS_BY_TYPE.local;
container.innerHTML = this.renderLocFields(idx, suffixes, { ...loc, type });
const conn = document.getElementById(`backup-location-${idx}-connection`); this.tagFieldsForSave(container);
if (conn) { this.filterEngineSelect(container, type, loc.engine);
conn.innerHTML = this.renderLocFields(idx, groups.connection, { ...loc, type }); this.applySshAuthVisibility(container);
this.tagFieldsForSave(conn); this.applyPathModeVisibility(container);
this.filterEngineSelect(conn, type, loc.engine);
this.applySshAuthVisibility(conn);
this.applyPathModeVisibility(conn);
}
// The Advanced tab's fields are type-dependent too (URI override only
// applies to some types), so rebuild it alongside the Connection tab.
const adv = document.getElementById(`backup-location-${idx}-advanced`);
if (adv) {
adv.innerHTML = groups.advanced.length
? this.renderLocFields(idx, groups.advanced, { ...loc, type })
: `<div class="backup-empty-state">No advanced options for this location type.</div>`;
this.tagFieldsForSave(adv);
}
this.enhanceEngineDetailsButton(); this.enhanceEngineDetailsButton();
} }
@ -1450,49 +1426,62 @@ class BackupPage {
KEEP_YEARLY: loc.keep_yearly KEEP_YEARLY: loc.keep_yearly
}; };
// Field metadata comes from configs.json (window.configData) via // Field metadata now comes from configs.json (window.configData): the
// locFieldMeta; the basic/advanced split is decided by the caller, which // generator emits CFG_BACKUP_LOC_N_* titles/descriptions plus an
// renders each group into its own tab (Connection vs Advanced). // "advanced" flag. BACKUP_LOC_FIELD_DEFS stays only as a fallback for a
let html = '<div class="config-fields">'; // sparse location.config that doesn't describe a field yet.
for (const suffix of suffixes) { const advancedFallback = new Set(['URI', 'SSH_PORT', 'APPEND_ONLY']);
const m = this.locFieldMeta(idx, suffix); const fieldMeta = (suffix) => {
if (!m.exists) continue;
const value = (locValueLookup[suffix] ?? '').toString();
html += ConfigShared.generateField(`config-${m.key}`, m.key, value, m.title, m.description, {}, {});
}
html += '</div>';
return html;
}
/* Resolve a location field's metadata. Source of truth is configs.json
(window.configData) titles/descriptions/options + a per-field "advanced"
flag; BACKUP_LOC_FIELD_DEFS is the fallback for sparse location.configs.
LOC_ADVANCED_SUFFIXES keeps the known overrides advanced even on legacy
locations whose config predates the **ADVANCED** marker. */
locFieldMeta(idx, suffix) {
const key = `CFG_BACKUP_LOC_${idx}_${suffix}`; const key = `CFG_BACKUP_LOC_${idx}_${suffix}`;
const cfg = window.configData?.config?.[key] || {}; const cfg = window.configData?.config?.[key] || {};
const def = BACKUP_LOC_FIELD_DEFS[suffix] || {}; const def = BACKUP_LOC_FIELD_DEFS[suffix] || {};
// Fallback set keeps these advanced even on legacy locations whose
// location.config predates the **ADVANCED** marker; configData can
// additionally flag others.
const advanced = advancedFallback.has(suffix) || cfg.advanced === true;
return { return {
key, key,
exists: !!(cfg.title || cfg.description || BACKUP_LOC_FIELD_DEFS[suffix]), exists: !!(cfg.title || cfg.description || BACKUP_LOC_FIELD_DEFS[suffix]),
title: cfg.title || def.title || suffix, title: cfg.title || def.title || suffix,
description: cfg.description ?? def.description ?? '', description: cfg.description ?? def.description ?? '',
advanced: LOC_ADVANCED_SUFFIXES.has(suffix) || cfg.advanced === true advanced
};
}; };
}
/* Split a type's fields into the Connection tab vs the Advanced tab. */ const renderField = (suffix) => {
locFieldGroups(idx, type) { const m = fieldMeta(suffix);
const suffixes = BACKUP_LOC_FIELDS_BY_TYPE[type] || BACKUP_LOC_FIELDS_BY_TYPE.local; const value = (locValueLookup[suffix] ?? '').toString();
const connection = []; return ConfigShared.generateField(`config-${m.key}`, m.key, value, m.title, m.description, {}, {});
};
const basic = [];
const advanced = []; const advanced = [];
for (const suffix of suffixes) { for (const suffix of suffixes) {
const m = this.locFieldMeta(idx, suffix); const m = fieldMeta(suffix);
if (!m.exists) continue; if (!m.exists) continue;
(m.advanced ? advanced : connection).push(suffix); (m.advanced ? advanced : basic).push(suffix);
} }
return { connection, advanced };
// Basics stay in the .config-fields grid (same as /config). Advanced
// fields (URI override, SSH port, append-only) tuck behind a full-width
// disclosure so the common case opens simple.
let html = '<div class="config-fields">';
html += basic.map(renderField).join('');
html += '</div>';
if (advanced.length) {
const secId = `backup-loc-${idx}-advanced`;
html += `
<button type="button" class="backup-loc-advanced-toggle" data-action="toggle-loc-advanced" data-target="${secId}" aria-expanded="false" aria-controls="${secId}">
<svg class="backup-loc-advanced-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
<span>Advanced</span>
</button>
<div class="config-fields backup-loc-advanced" id="${secId}" hidden>
${advanced.map(renderField).join('')}
</div>
`;
}
return html;
} }
tagFieldsForSave(container) { tagFieldsForSave(container) {