Compare commits
2 Commits
4b8f2c698c
...
8a99ebd080
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a99ebd080 | ||
|
|
6da8f80477 |
@ -723,47 +723,48 @@
|
|||||||
border-top: 1px dashed rgba(var(--text-rgb), 0.08);
|
border-top: 1px dashed rgba(var(--text-rgb), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full-width "Advanced" disclosure in the location editor's Connection
|
/* Tabbed location editor (Connection | Retention | Advanced). Splits the
|
||||||
section. Reveals the advanced fields (URI override, SSH port, append-only). */
|
formerly long single form into one panel per concern. */
|
||||||
.backup-loc-advanced-toggle {
|
.backup-loc-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
gap: 8px;
|
border-bottom: 1px solid rgba(var(--text-rgb), 0.10);
|
||||||
width: 100%;
|
margin-bottom: 16px;
|
||||||
margin-top: 14px;
|
}
|
||||||
padding: 10px 12px;
|
|
||||||
background: rgba(var(--text-rgb), 0.03);
|
.backup-loc-tab {
|
||||||
border: 1px solid rgba(var(--text-rgb), 0.08);
|
appearance: none;
|
||||||
border-radius: 8px;
|
background: transparent;
|
||||||
color: var(--text-primary);
|
border: none;
|
||||||
|
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: background 0.15s ease;
|
transition: color 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backup-loc-advanced-toggle:hover {
|
.backup-loc-tab:hover {
|
||||||
background: rgba(var(--text-rgb), 0.06);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.backup-loc-advanced-chevron {
|
.backup-loc-tab.active {
|
||||||
transition: transform 0.15s ease;
|
color: var(--accent);
|
||||||
flex-shrink: 0;
|
border-bottom-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.backup-loc-advanced-toggle.open .backup-loc-advanced-chevron {
|
.backup-loc-tab-panel[hidden] {
|
||||||
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;
|
||||||
|
|||||||
@ -62,6 +62,11 @@ 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)) {
|
||||||
@ -198,15 +203,19 @@ class BackupPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const advToggle = e.target.closest('[data-action="toggle-loc-advanced"]');
|
const locTab = e.target.closest('[data-action="loc-tab"]');
|
||||||
if (advToggle) {
|
if (locTab) {
|
||||||
const sec = document.getElementById(advToggle.dataset.target);
|
const tabIdx = locTab.dataset.loc;
|
||||||
if (sec) {
|
const tabName = locTab.dataset.tab;
|
||||||
const show = sec.hasAttribute('hidden');
|
const root = locTab.closest('.backup-location-config') || document;
|
||||||
sec.toggleAttribute('hidden', !show);
|
root.querySelectorAll(`[data-action="loc-tab"][data-loc="${tabIdx}"]`).forEach(b => {
|
||||||
advToggle.setAttribute('aria-expanded', show ? 'true' : 'false');
|
const on = b === locTab;
|
||||||
advToggle.classList.toggle('open', show);
|
b.classList.toggle('active', on);
|
||||||
}
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -659,7 +668,7 @@ class BackupPage {
|
|||||||
|
|
||||||
renderLocationDetailsBody(l) {
|
renderLocationDetailsBody(l) {
|
||||||
const idx = l.idx;
|
const idx = l.idx;
|
||||||
const connectionFields = BACKUP_LOC_FIELDS_BY_TYPE[l.type] || BACKUP_LOC_FIELDS_BY_TYPE.local;
|
const groups = this.locFieldGroups(idx, l.type);
|
||||||
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 || '') : '',
|
||||||
@ -668,34 +677,37 @@ 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="domains-wrapper">
|
<div class="backup-loc-tabs" role="tablist">
|
||||||
<div class="domains-header">
|
${tab('connection', 'Connection')}
|
||||||
<div>
|
${tab('retention', 'Retention')}
|
||||||
<h3>Connection</h3>
|
${tab('advanced', 'Advanced')}
|
||||||
<p class="category-description">How LibrePortal connects to this storage location.</p>
|
</div>
|
||||||
</div>
|
<div class="backup-loc-tab-panel" data-tab-panel="connection" data-loc="${idx}">
|
||||||
</div>
|
<p class="category-description">How LibrePortal connects to this storage location.</p>
|
||||||
<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, connectionFields, l)}
|
${this.renderLocFields(idx, groups.connection, l)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="backup-loc-tab-panel" data-tab-panel="retention" data-loc="${idx}" hidden>
|
||||||
<div class="config-category backup-location-config">
|
<p class="category-description">When to delete old backups from this location.</p>
|
||||||
<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>
|
|
||||||
</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}">
|
||||||
@ -750,15 +762,27 @@ 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 suffixes = BACKUP_LOC_FIELDS_BY_TYPE[type] || BACKUP_LOC_FIELDS_BY_TYPE.local;
|
const groups = this.locFieldGroups(idx, type);
|
||||||
container.innerHTML = this.renderLocFields(idx, suffixes, { ...loc, type });
|
|
||||||
this.tagFieldsForSave(container);
|
const conn = document.getElementById(`backup-location-${idx}-connection`);
|
||||||
this.filterEngineSelect(container, type, loc.engine);
|
if (conn) {
|
||||||
this.applySshAuthVisibility(container);
|
conn.innerHTML = this.renderLocFields(idx, groups.connection, { ...loc, type });
|
||||||
this.applyPathModeVisibility(container);
|
this.tagFieldsForSave(conn);
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1426,62 +1450,49 @@ class BackupPage {
|
|||||||
KEEP_YEARLY: loc.keep_yearly
|
KEEP_YEARLY: loc.keep_yearly
|
||||||
};
|
};
|
||||||
|
|
||||||
// Field metadata now comes from configs.json (window.configData): the
|
// Field metadata comes from configs.json (window.configData) via
|
||||||
// generator emits CFG_BACKUP_LOC_N_* titles/descriptions plus an
|
// locFieldMeta; the basic/advanced split is decided by the caller, which
|
||||||
// "advanced" flag. BACKUP_LOC_FIELD_DEFS stays only as a fallback for a
|
// renders each group into its own tab (Connection vs Advanced).
|
||||||
// sparse location.config that doesn't describe a field yet.
|
let html = '<div class="config-fields">';
|
||||||
const advancedFallback = new Set(['URI', 'SSH_PORT', 'APPEND_ONLY']);
|
for (const suffix of suffixes) {
|
||||||
const fieldMeta = (suffix) => {
|
const m = this.locFieldMeta(idx, suffix);
|
||||||
const key = `CFG_BACKUP_LOC_${idx}_${suffix}`;
|
if (!m.exists) continue;
|
||||||
const cfg = window.configData?.config?.[key] || {};
|
|
||||||
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 {
|
|
||||||
key,
|
|
||||||
exists: !!(cfg.title || cfg.description || BACKUP_LOC_FIELD_DEFS[suffix]),
|
|
||||||
title: cfg.title || def.title || suffix,
|
|
||||||
description: cfg.description ?? def.description ?? '',
|
|
||||||
advanced
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderField = (suffix) => {
|
|
||||||
const m = fieldMeta(suffix);
|
|
||||||
const value = (locValueLookup[suffix] ?? '').toString();
|
const value = (locValueLookup[suffix] ?? '').toString();
|
||||||
return ConfigShared.generateField(`config-${m.key}`, m.key, value, m.title, m.description, {}, {});
|
html += ConfigShared.generateField(`config-${m.key}`, m.key, value, m.title, m.description, {}, {});
|
||||||
};
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
const basic = [];
|
/* 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 cfg = window.configData?.config?.[key] || {};
|
||||||
|
const def = BACKUP_LOC_FIELD_DEFS[suffix] || {};
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
exists: !!(cfg.title || cfg.description || BACKUP_LOC_FIELD_DEFS[suffix]),
|
||||||
|
title: cfg.title || def.title || suffix,
|
||||||
|
description: cfg.description ?? def.description ?? '',
|
||||||
|
advanced: LOC_ADVANCED_SUFFIXES.has(suffix) || cfg.advanced === true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Split a type's fields into the Connection tab vs the Advanced tab. */
|
||||||
|
locFieldGroups(idx, type) {
|
||||||
|
const suffixes = BACKUP_LOC_FIELDS_BY_TYPE[type] || BACKUP_LOC_FIELDS_BY_TYPE.local;
|
||||||
|
const connection = [];
|
||||||
const advanced = [];
|
const advanced = [];
|
||||||
for (const suffix of suffixes) {
|
for (const suffix of suffixes) {
|
||||||
const m = fieldMeta(suffix);
|
const m = this.locFieldMeta(idx, suffix);
|
||||||
if (!m.exists) continue;
|
if (!m.exists) continue;
|
||||||
(m.advanced ? advanced : basic).push(suffix);
|
(m.advanced ? advanced : connection).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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user