max / goingson
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
5 files changed,
+94 insertions,
-183 deletions
| @@ -173,98 +173,6 @@ | |||
| 173 | 173 | return GoingsOn.api.forms.renderFields(specs, idPrefix); | |
| 174 | 174 | } | |
| 175 | 175 | ||
| 176 | - | /** | |
| 177 | - | * Render a single form field as an HTML string. | |
| 178 | - | * | |
| 179 | - | * @deprecated Superseded by {@link renderFields}, which renders the same shape | |
| 180 | - | * from makeover-webview instead. Being retired call site by call site; do not | |
| 181 | - | * add new callers. | |
| 182 | - | * @param {Object} field - Field definition | |
| 183 | - | * @param {string} field.kind - 'text' | 'email' | 'number' | 'password' | 'date' | 'time' | 'datetime-local' | 'hidden' | 'select' | 'textarea' | 'checkbox' | |
| 184 | - | * @param {string} field.name - Form input name | |
| 185 | - | * @param {string} [field.label] - Field label | |
| 186 | - | * @param {string} [field.id] - Input id (defaults to field.name) | |
| 187 | - | * @param {*} [field.value] - Current value | |
| 188 | - | * @param {string} [field.placeholder] | |
| 189 | - | * @param {boolean} [field.required] | |
| 190 | - | * @param {Array<{value, label, selected?}>} [field.options] - For select. A value | |
| 191 | - | * matching no option is kept as a generated leading option rather than being | |
| 192 | - | * dropped to the browser's first-option fallback. | |
| 193 | - | * @param {string} [field.hint] - Help text under input (HTML-escaped) | |
| 194 | - | * @param {string} [field.hintExtraHtml] - Raw HTML appended after hint (NOT escaped; caller must sanitize) | |
| 195 | - | * @param {string} [field.error] - Error text (renders has-error variant) | |
| 196 | - | * @param {boolean} [field.preview] - Whether to render a preview slot under the input | |
| 197 | - | * @returns {string} - HTML string for the field group | |
| 198 | - | */ | |
| 199 | - | function renderFormField(field) { | |
| 200 | - | const utils = GoingsOn.utils; | |
| 201 | - | const esc = utils.escapeHtml; | |
| 202 | - | const escAttr = utils.escapeAttrValue; | |
| 203 | - | const kind = field.kind || field.type || 'text'; | |
| 204 | - | const inputId = field.id || field.name; | |
| 205 | - | const value = field.value ?? ''; | |
| 206 | - | const required = field.required ? 'required' : ''; | |
| 207 | - | const placeholder = field.placeholder ? `placeholder="${escAttr(field.placeholder)}"` : ''; | |
| 208 | - | const extraAttrs = field.attrs | |
| 209 | - | ? Object.entries(field.attrs).map(([k, v]) => `${k}="${escAttr(String(v))}"`).join(' ') | |
| 210 | - | : ''; | |
| 211 | - | ||
| 212 | - | if (kind === 'hidden') { | |
| 213 | - | return `<input type="hidden" name="${field.name}" value="${escAttr(value)}">`; | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | let inputHtml = ''; | |
| 217 | - | let isCheckbox = false; | |
| 218 | - | ||
| 219 | - | switch (kind) { | |
| 220 | - | case 'textarea': | |
| 221 | - | inputHtml = `<textarea class="field" id="${inputId}" name="${field.name}" ${required} ${placeholder} ${extraAttrs}>${esc(value)}</textarea>`; | |
| 222 | - | break; | |
| 223 | - | case 'select': { | |
| 224 | - | const options = field.options || []; | |
| 225 | - | // A select fed a value no option carries renders with nothing | |
| 226 | - | // selected, so the browser falls back to the first option and the | |
| 227 | - | // next save writes a value nobody chose (the backup retention | |
| 228 | - | // default of 10 against a 1/3/7/14/0 list did exactly that). | |
| 229 | - | // Carry the stray value as its own option instead: it stays | |
| 230 | - | // selected, it round-trips through a save untouched, and it is | |
| 231 | - | // visible rather than silent. Compared as strings because callers | |
| 232 | - | // routinely pass a number for value and strings for opt.value. | |
| 233 | - | const matched = options.some(opt => opt.selected || String(opt.value) === String(value)); | |
| 234 | - | let strayHtml = ''; | |
| 235 | - | if (value !== '' && !matched) { | |
| 236 | - | console.warn(`renderFormField: select "${field.name}" has no option for value ${JSON.stringify(value)}; keeping the value as-is.`); | |
| 237 | - | strayHtml = `<option value="${escAttr(String(value))}" selected data-unmatched="true">${esc(String(value))}</option>`; | |
| 238 | - | } | |
| 239 | - | const optionsHtml = options.map(opt => { | |
| 240 | - | const selected = (opt.selected || opt.value === value) ? 'selected' : ''; | |
| 241 | - | return `<option value="${escAttr(opt.value)}" ${selected}>${esc(opt.label)}</option>`; | |
| 242 | - | }).join(''); | |
| 243 | - | inputHtml = `<select class="field" id="${inputId}" name="${field.name}" ${required} ${extraAttrs}>${strayHtml}${optionsHtml}</select>`; | |
| 244 | - | break; | |
| 245 | - | } | |
| 246 | - | case 'checkbox': | |
| 247 | - | isCheckbox = true; | |
| 248 | - | inputHtml = `<label class="form-checkbox-label"><input type="checkbox" id="${inputId}" name="${field.name}" ${value ? 'checked' : ''} ${extraAttrs}><span>${esc(field.label || '')}</span></label>`; | |
| 249 | - | break; | |
| 250 | - | default: | |
| 251 | - | inputHtml = `<input type="${kind}" class="field" id="${inputId}" name="${field.name}" ${required} ${placeholder} value="${escAttr(value)}" ${extraAttrs}>`; | |
| 252 | - | } | |
| 253 | - | ||
| 254 | - | const hintText = field.hint ? `<div class="form-hint">${esc(field.hint)}</div>` : ''; | |
| 255 | - | const hintExtra = field.hintExtraHtml || ''; | |
| 256 | - | const hintHtml = hintText + hintExtra; | |
| 257 | - | const previewHtml = field.preview ? `<div id="${inputId}-preview" class="form-hint form-hint--preview"></div>` : ''; | |
| 258 | - | const errorHtml = field.error ? `<div class="form-error visible">${esc(field.error)}</div>` : ''; | |
| 259 | - | const errorClass = field.error ? ' has-error' : ''; | |
| 260 | - | ||
| 261 | - | if (isCheckbox) { | |
| 262 | - | return `<div class="form-group${errorClass}">${inputHtml}${hintHtml}${errorHtml}</div>`; | |
| 263 | - | } | |
| 264 | - | const labelHtml = field.label ? `<label class="form-label" for="${inputId}">${esc(field.label)}</label>` : ''; | |
| 265 | - | return `<div class="form-group${errorClass}">${labelHtml}${inputHtml}${hintHtml}${previewHtml}${errorHtml}</div>`; | |
| 266 | - | } | |
| 267 | - | ||
| 268 | 176 | // Context Menu | |
| 269 | 177 | ||
| 270 | 178 | let contextMenuElement = null; | |
| @@ -677,7 +585,6 @@ | |||
| 677 | 585 | // View helpers | |
| 678 | 586 | renderEmptyState, | |
| 679 | 587 | emptyStateIcon, | |
| 680 | - | renderFormField, | |
| 681 | 588 | renderFields, | |
| 682 | 589 | ||
| 683 | 590 | // API wrapper |
| @@ -35,36 +35,21 @@ | |||
| 35 | 35 | * @param {string} opts.submitLabel - Submit button label | |
| 36 | 36 | * @returns {string} HTML string for the form | |
| 37 | 37 | */ | |
| 38 | - | function buildAccountFormHtml(opts) { | |
| 38 | + | async function buildAccountFormHtml(opts) { | |
| 39 | 39 | const { formId, idPrefix, submitAct, submitId, values, isEdit, submitLabel } = opts; | |
| 40 | 40 | ||
| 41 | 41 | const passwordLabel = isEdit ? 'Password (leave empty to keep current)' : 'Password'; | |
| 42 | - | const passwordRequired = isEdit ? '' : 'required'; | |
| 43 | 42 | const passwordPlaceholder = isEdit ? 'Enter new password or leave empty' : 'your password'; | |
| 44 | 43 | ||
| 45 | 44 | const archiveHint = isEdit | |
| 46 | 45 | ? 'Use Test Connection to see available folders on this account.' | |
| 47 | 46 | : 'Gmail: [Gmail]/All Mail, Fastmail: Archive. Use Test Connection to see available folders.'; | |
| 48 | 47 | ||
| 49 | - | const syncOptionsHtml = SYNC_INTERVAL_OPTIONS.map(opt => { | |
| 50 | - | let selected = ''; | |
| 51 | - | if (isEdit) { | |
| 52 | - | // For edit: match against current value (null maps to '') | |
| 53 | - | const current = values.syncIntervalMinutes != null ? String(values.syncIntervalMinutes) : ''; | |
| 54 | - | selected = (opt.value === current) ? 'selected' : ''; | |
| 55 | - | } else { | |
| 56 | - | // For add: default to 15 minutes | |
| 57 | - | selected = (opt.value === '15') ? 'selected' : ''; | |
| 58 | - | } | |
| 59 | - | return `<option value="${escAttr(opt.value)}" ${selected}>${esc(opt.label)}</option>`; | |
| 60 | - | }).join(''); | |
| 61 | - | ||
| 62 | 48 | // For new accounts, check if server fields have been pre-filled (edit mode always shows them) | |
| 63 | 49 | const hasServerValues = isEdit || !!(values.imapServer || values.smtpServer); | |
| 64 | 50 | const advancedExpanded = hasServerValues ? 'expanded' : ''; | |
| 65 | 51 | const advancedHidden = hasServerValues ? '' : 'hidden'; | |
| 66 | 52 | ||
| 67 | - | const ff = GoingsOn.ui.renderFormField; | |
| 68 | 53 | const syncIntervalOpts = SYNC_INTERVAL_OPTIONS.map(opt => { | |
| 69 | 54 | let selected; | |
| 70 | 55 | if (isEdit) { | |
| @@ -76,28 +61,47 @@ | |||
| 76 | 61 | return { value: opt.value, label: opt.label, selected }; | |
| 77 | 62 | }); | |
| 78 | 63 | ||
| 64 | + | // Ids are derived as `${idPrefix}-${name}` now rather than being spelled | |
| 65 | + | // per field, so the lookups in autodetect below follow the field names. | |
| 66 | + | // That is why they read `-imap_server` and not `-imap-server`. | |
| 67 | + | const f = await GoingsOn.ui.renderFields([ | |
| 68 | + | { kind: 'text', name: 'account_name', label: 'Account Name', value: values.accountName || '', placeholder: 'Personal, Work, etc.', required: true }, | |
| 69 | + | { kind: 'text', name: 'username', label: 'Username', value: values.username || '', placeholder: 'Usually your email address', required: true }, | |
| 70 | + | { kind: 'password', name: 'password', label: passwordLabel, placeholder: passwordPlaceholder, required: !isEdit }, | |
| 71 | + | { kind: 'text', name: 'archive_folder_name', label: 'Archive Folder Name', value: values.archiveFolderName || 'Archive', placeholder: 'Archive', hint: archiveHint }, | |
| 72 | + | // The signature textarea used to ask for three rows through an extra | |
| 73 | + | // attribute. It takes the default height now; the emitter carries no | |
| 74 | + | // arbitrary attributes and rows is presentation. | |
| 75 | + | { kind: 'textarea', name: 'email_signature', label: 'Email Signature', value: values.emailSignature || '', placeholder: '-- \nYour Name', hint: 'Appended to outbound emails. Plain text only.' }, | |
| 76 | + | { kind: 'text', name: 'imap_server', label: 'IMAP Server', value: values.imapServer || '', placeholder: 'imap.example.com', required: true }, | |
| 77 | + | { kind: 'number', name: 'imap_port', label: 'IMAP Port', value: String(values.imapPort || 993), required: true }, | |
| 78 | + | { kind: 'text', name: 'smtp_server', label: 'SMTP Server', value: values.smtpServer || '', placeholder: 'smtp.example.com', required: true }, | |
| 79 | + | { kind: 'number', name: 'smtp_port', label: 'SMTP Port', value: String(values.smtpPort || 587), required: true }, | |
| 80 | + | { kind: 'select', name: 'sync_interval_minutes', label: 'Auto-sync Interval', options: syncIntervalOpts, hint: 'Automatically check for new emails at this interval.' }, | |
| 81 | + | ], idPrefix); | |
| 82 | + | ||
| 79 | 83 | return ` | |
| 80 | 84 | <form id="${formId}" data-submit="${escAttr(submitAct)}" data-a1="@event"${submitId ? ` data-a2="${escAttr(submitId)}"` : ''}> | |
| 81 | - | ${ff({ kind: 'text', name: 'account_name', id: `${idPrefix}-name`, label: 'Account Name', value: values.accountName || '', placeholder: 'Personal, Work, etc.', required: true })} | |
| 85 | + | ${f['account_name']} | |
| 82 | 86 | <div class="form-group"> | |
| 83 | 87 | <label class="form-label" for="${idPrefix}-email">Email Address</label> | |
| 84 | 88 | <input type="email" class="field" id="${idPrefix}-email" name="email_address" required placeholder="you@example.com" value="${escAttr(values.emailAddress || '')}"> | |
| 85 | 89 | <div id="${idPrefix}-detect-status" class="email-detect-status"></div> | |
| 86 | 90 | <div id="${idPrefix}-detect-note" class="email-detect-note hidden"></div> | |
| 87 | 91 | </div> | |
| 88 | - | ${ff({ kind: 'text', name: 'username', id: `${idPrefix}-username`, label: 'Username', value: values.username || '', placeholder: 'Usually your email address', required: true })} | |
| 89 | - | ${ff({ kind: 'password', name: 'password', id: `${idPrefix}-password`, label: passwordLabel, placeholder: passwordPlaceholder, required: !isEdit })} | |
| 90 | - | ${ff({ kind: 'text', name: 'archive_folder_name', id: `${idPrefix}-archive-folder`, label: 'Archive Folder Name', value: values.archiveFolderName || 'Archive', placeholder: 'Archive', hint: archiveHint })} | |
| 91 | - | ${ff({ kind: 'textarea', name: 'email_signature', id: `${idPrefix}-signature`, label: 'Email Signature', value: values.emailSignature || '', placeholder: '-- \nYour Name', hint: 'Appended to outbound emails. Plain text only.', attrs: { rows: 3 } })} | |
| 92 | + | ${f['username']} | |
| 93 | + | ${f['password']} | |
| 94 | + | ${f['archive_folder_name']} | |
| 95 | + | ${f['email_signature']} | |
| 92 | 96 | <button type="button" class="form-more-toggle ${advancedExpanded}" data-act="ui.toggleExpand" data-a1="@el">Advanced settings</button> | |
| 93 | 97 | <div class="${advancedHidden}"> | |
| 94 | 98 | <div class="form-grid-2"> | |
| 95 | - | ${ff({ kind: 'text', name: 'imap_server', id: `${idPrefix}-imap-server`, label: 'IMAP Server', value: values.imapServer || '', placeholder: 'imap.example.com', required: true })} | |
| 96 | - | ${ff({ kind: 'number', name: 'imap_port', id: `${idPrefix}-imap-port`, label: 'IMAP Port', value: values.imapPort || 993, required: true })} | |
| 99 | + | ${f['imap_server']} | |
| 100 | + | ${f['imap_port']} | |
| 97 | 101 | </div> | |
| 98 | 102 | <div class="form-grid-2"> | |
| 99 | - | ${ff({ kind: 'text', name: 'smtp_server', id: `${idPrefix}-smtp-server`, label: 'SMTP Server', value: values.smtpServer || '', placeholder: 'smtp.example.com', required: true })} | |
| 100 | - | ${ff({ kind: 'number', name: 'smtp_port', id: `${idPrefix}-smtp-port`, label: 'SMTP Port', value: values.smtpPort || 587, required: true })} | |
| 103 | + | ${f['smtp_server']} | |
| 104 | + | ${f['smtp_port']} | |
| 101 | 105 | </div> | |
| 102 | 106 | <div class="form-group"> | |
| 103 | 107 | <label class="form-checkbox-label"> | |
| @@ -106,7 +110,7 @@ | |||
| 106 | 110 | </label> | |
| 107 | 111 | <div class="form-hint">Show a system notification when new emails arrive during auto-sync. Off by default.</div> | |
| 108 | 112 | </div> | |
| 109 | - | ${ff({ kind: 'select', name: 'sync_interval_minutes', id: `${idPrefix}-sync-interval`, label: 'Auto-sync Interval', options: syncIntervalOpts, hint: 'Automatically check for new emails at this interval.' })} | |
| 113 | + | ${f['sync_interval_minutes']} | |
| 110 | 114 | </div> | |
| 111 | 115 | <div class="form-actions"> | |
| 112 | 116 | <button type="button" class="button button--secondary" data-act="emails.refreshAccountsView">Cancel</button> | |
| @@ -182,12 +186,12 @@ | |||
| 182 | 186 | ||
| 183 | 187 | lastDetectedDomain = domain; | |
| 184 | 188 | ||
| 185 | - | const imapEl = document.getElementById(`${idPrefix}-imap-server`); | |
| 186 | - | const imapPortEl = document.getElementById(`${idPrefix}-imap-port`); | |
| 187 | - | const smtpEl = document.getElementById(`${idPrefix}-smtp-server`); | |
| 188 | - | const smtpPortEl = document.getElementById(`${idPrefix}-smtp-port`); | |
| 189 | + | const imapEl = document.getElementById(`${idPrefix}-imap_server`); | |
| 190 | + | const imapPortEl = document.getElementById(`${idPrefix}-imap_port`); | |
| 191 | + | const smtpEl = document.getElementById(`${idPrefix}-smtp_server`); | |
| 192 | + | const smtpPortEl = document.getElementById(`${idPrefix}-smtp_port`); | |
| 189 | 193 | const usernameEl = document.getElementById(`${idPrefix}-username`); | |
| 190 | - | const archiveEl = document.getElementById(`${idPrefix}-archive-folder`); | |
| 194 | + | const archiveEl = document.getElementById(`${idPrefix}-archive_folder_name`); | |
| 191 | 195 | ||
| 192 | 196 | // Auto-fill server fields | |
| 193 | 197 | if (imapEl && (!imapEl.value || imapEl.value === 'imap.example.com')) imapEl.value = settings.imap; | |
| @@ -375,7 +379,7 @@ | |||
| 375 | 379 | ` | |
| 376 | 380 | : ''; | |
| 377 | 381 | ||
| 378 | - | const formHtml = buildAccountFormHtml({ | |
| 382 | + | const formHtml = await buildAccountFormHtml({ | |
| 379 | 383 | formId: 'email-account-form', | |
| 380 | 384 | idPrefix: 'acct', | |
| 381 | 385 | submitAct: 'emails.createAccount', | |
| @@ -429,7 +433,7 @@ | |||
| 429 | 433 | return; | |
| 430 | 434 | } | |
| 431 | 435 | ||
| 432 | - | const content = buildAccountFormHtml({ | |
| 436 | + | const content = await buildAccountFormHtml({ | |
| 433 | 437 | formId: 'edit-email-account-form', | |
| 434 | 438 | idPrefix: 'edit-acct', | |
| 435 | 439 | submitAct: 'emails.updateAccount', submitId: id, |
| @@ -245,7 +245,24 @@ | |||
| 245 | 245 | { value: 0, label: 'Keep all backups' }, | |
| 246 | 246 | ]; | |
| 247 | 247 | ||
| 248 | - | const ff = GoingsOn.ui.renderFormField; | |
| 248 | + | const f = await GoingsOn.ui.renderFields([ | |
| 249 | + | { | |
| 250 | + | kind: 'select', | |
| 251 | + | name: 'backup-frequency', | |
| 252 | + | label: 'Backup Frequency', | |
| 253 | + | value: String(settings.backupFrequencyMinutes), | |
| 254 | + | options: frequencyOptions.map(o => ({ value: String(o.value), label: o.label })), | |
| 255 | + | }, | |
| 256 | + | { | |
| 257 | + | kind: 'select', | |
| 258 | + | name: 'backup-retention', | |
| 259 | + | label: 'Retention Policy', | |
| 260 | + | value: String(settings.maxBackupsToKeep), | |
| 261 | + | options: retentionOptions.map(o => ({ value: String(o.value), label: o.label })), | |
| 262 | + | hint: 'Older backups are automatically deleted to save space.', | |
| 263 | + | }, | |
| 264 | + | ]); | |
| 265 | + | ||
| 249 | 266 | const content = ` | |
| 250 | 267 | <p class="export-desc"> | |
| 251 | 268 | Automatic backups protect your data by creating compressed snapshots on a schedule. | |
| @@ -259,24 +276,9 @@ | |||
| 259 | 276 | </label> | |
| 260 | 277 | </div> | |
| 261 | 278 | ||
| 262 | - | ${ff({ | |
| 263 | - | kind: 'select', | |
| 264 | - | name: 'backup-frequency', | |
| 265 | - | id: 'backup-frequency', | |
| 266 | - | label: 'Backup Frequency', | |
| 267 | - | value: settings.backupFrequencyMinutes, | |
| 268 | - | options: frequencyOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.backupFrequencyMinutes === o.value })), | |
| 269 | - | })} | |
| 279 | + | ${f['backup-frequency']} | |
| 270 | 280 | ||
| 271 | - | ${ff({ | |
| 272 | - | kind: 'select', | |
| 273 | - | name: 'backup-retention', | |
| 274 | - | id: 'backup-retention', | |
| 275 | - | label: 'Retention Policy', | |
| 276 | - | value: settings.maxBackupsToKeep, | |
| 277 | - | options: retentionOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.maxBackupsToKeep === o.value })), | |
| 278 | - | hint: 'Older backups are automatically deleted to save space.', | |
| 279 | - | })} | |
| 281 | + | ${f['backup-retention']} | |
| 280 | 282 | ||
| 281 | 283 | <div class="export-note"> | |
| 282 | 284 | <p class="export-note-text">${esc(lastBackupText)}</p> |
| @@ -91,26 +91,24 @@ | |||
| 91 | 91 | `; | |
| 92 | 92 | } catch (_) {} | |
| 93 | 93 | ||
| 94 | - | // Still on the old renderer, and the reason is worth stating: this | |
| 95 | - | // select carries an inline onchange through `attrs`, and the | |
| 96 | - | // emitter has no way to carry an app's hook attributes. It cannot | |
| 97 | - | // become a data-change either, because the delegated dispatcher | |
| 98 | - | // reads el.value off whichever element holds the attribute, so | |
| 99 | - | // moving it to a wrapper passes undefined. Blocked until the | |
| 100 | - | // emitter can carry them. | |
| 101 | - | const intervalField = GoingsOn.ui.renderFormField({ | |
| 94 | + | // The change handler used to be an inline onchange passed through | |
| 95 | + | // as an extra attribute. It is bound by id after render instead, | |
| 96 | + | // below, which is what the modal form builder already does for | |
| 97 | + | // every handler it wires. A data-change would not work here: the | |
| 98 | + | // delegated dispatcher reads el.value off whichever element carries | |
| 99 | + | // the attribute, and the emitter puts attributes on the control it | |
| 100 | + | // is describing, not on markup the app supplies. | |
| 101 | + | const interval = await GoingsOn.ui.renderFields([{ | |
| 102 | 102 | kind: 'select', | |
| 103 | 103 | name: 'sync-interval', | |
| 104 | - | id: 'sync-interval', | |
| 105 | 104 | label: 'Sync Interval', | |
| 106 | - | value: status.syncIntervalMinutes, | |
| 107 | - | attrs: { onchange: 'GoingsOn.settings.updateSyncSettings()' }, | |
| 105 | + | value: String(status.syncIntervalMinutes), | |
| 108 | 106 | options: intervalOptions.map(m => ({ | |
| 109 | 107 | value: String(m), | |
| 110 | 108 | label: m === 1 ? '1 minute' : m + ' minutes', | |
| 111 | - | selected: status.syncIntervalMinutes === m, | |
| 112 | 109 | })), | |
| 113 | - | }); | |
| 110 | + | }]); | |
| 111 | + | const intervalField = interval['sync-interval']; | |
| 114 | 112 | ||
| 115 | 113 | sectionContent = ` | |
| 116 | 114 | <div class="sync-status-row"> | |
| @@ -161,6 +159,13 @@ | |||
| 161 | 159 | </div> | |
| 162 | 160 | `; | |
| 163 | 161 | ||
| 162 | + | // Bind the sync-interval handler now the select is in the document. | |
| 163 | + | // Replaces the inline onchange it used to carry as an extra attribute. | |
| 164 | + | const intervalEl = document.getElementById('sync-interval'); | |
| 165 | + | if (intervalEl) { | |
| 166 | + | intervalEl.addEventListener('change', () => updateSyncSettings()); | |
| 167 | + | } | |
| 168 | + | ||
| 164 | 169 | // After render: check subscription status and show banner if needed | |
| 165 | 170 | if (status.encryptionReady) { | |
| 166 | 171 | checkSubscriptionBanner(); |
| @@ -225,7 +225,23 @@ | |||
| 225 | 225 | { value: 0, label: 'Keep all backups' }, | |
| 226 | 226 | ]; | |
| 227 | 227 | ||
| 228 | - | const renderFormField = GoingsOn.ui.renderFormField; | |
| 228 | + | const f = await GoingsOn.ui.renderFields([ | |
| 229 | + | { | |
| 230 | + | kind: 'select', | |
| 231 | + | name: 'backup-frequency', | |
| 232 | + | label: 'Backup Frequency', | |
| 233 | + | value: String(settings.backupFrequencyMinutes), | |
| 234 | + | options: frequencyOptions.map(o => ({ value: String(o.value), label: o.label })), | |
| 235 | + | }, | |
| 236 | + | { | |
| 237 | + | kind: 'select', | |
| 238 | + | name: 'backup-retention', | |
| 239 | + | label: 'Retention Policy', | |
| 240 | + | value: String(settings.maxBackupsToKeep), | |
| 241 | + | options: retentionOptions.map(o => ({ value: String(o.value), label: o.label })), | |
| 242 | + | hint: 'Older backups are automatically deleted to save space.', | |
| 243 | + | }, | |
| 244 | + | ]); | |
| 229 | 245 | // Frequency and retention sit behind a disclosure: the defaults are | |
| 230 | 246 | // right for almost everyone, so the section reads as one on/off | |
| 231 | 247 | // choice until someone asks for more. Open it when the settings are | |
| @@ -254,31 +270,8 @@ | |||
| 254 | 270 | </div> | |
| 255 | 271 | <details class="settings-disclosure"${customized ? ' open' : ''}> | |
| 256 | 272 | <summary class="settings-disclosure-toggle">Customize</summary> | |
| 257 | - | ${renderFormField({ | |
| 258 | - | kind: 'select', | |
| 259 | - | name: 'backup-frequency', | |
| 260 | - | id: 'backup-frequency', | |
| 261 | - | label: 'Backup Frequency', | |
| 262 | - | value: settings.backupFrequencyMinutes, | |
| 263 | - | options: frequencyOptions.map(o => ({ | |
| 264 | - | value: String(o.value), | |
| 265 | - | label: o.label, | |
| 266 | - | selected: settings.backupFrequencyMinutes === o.value, | |
| 267 | - | })), | |
| 268 | - | })} | |
| 269 | - | ${renderFormField({ | |
| 270 | - | kind: 'select', | |
| 271 | - | name: 'backup-retention', | |
| 272 | - | id: 'backup-retention', | |
| 273 | - | label: 'Retention Policy', | |
| 274 | - | value: settings.maxBackupsToKeep, | |
| 275 | - | options: retentionOptions.map(o => ({ | |
| 276 | - | value: String(o.value), | |
| 277 | - | label: o.label, | |
| 278 | - | selected: settings.maxBackupsToKeep === o.value, | |
| 279 | - | })), | |
| 280 | - | hint: 'Older backups are automatically deleted to save space.', | |
| 281 | - | })} | |
| 273 | + | ${f['backup-frequency']} | |
| 274 | + | ${f['backup-retention']} | |
| 282 | 275 | </details> | |
| 283 | 276 | <div class="settings-actions-row settings-actions-row--center"> | |
| 284 | 277 | <button class="button button--primary" data-act="export.saveBackupSettings">Save Backup Settings</button> |