refactor: extract shared utilities and standardize page patterns

- Add fmtBytes() and csvToArr() helpers to hoover/helpers.js
- Replace inline async patterns with ActionButton/ConfirmDelete in wireguard.js
- Convert addDomain/editDomain to QuickModal + apiSubmit in proxy.js
- Convert settingsModal handlers to formAction in certs.js
- Remove redundant synced handling from dhcp.js apply button
- Add onComplete callback to ConfirmDelete (fixes users.js onRefresh bug)
- Fix passkeys.js ActionCell/Table usage (invalid component API)
- Remove duplicate fmtBytes from dashboard.js
This commit is contained in:
2026-07-28 17:32:51 +00:00
parent 244576b8eb
commit 8bb3619ddc
10 changed files with 156 additions and 220 deletions
+6 -2
View File
@@ -82,6 +82,7 @@ export function Card(props = {}) {
* @param {string} [props.label] - Button text (default: 'Remove')
* @param {object} [props.body] - Optional JSON body to send with DELETE
* @param {string} [props.deleteKey] - Unique ID for pending-delete row styling
* @param {function} [props.onComplete] - Callback after successful deletion
*/
export function ConfirmDelete(props = {}) {
const opts = { method: 'DELETE' };
@@ -118,13 +119,16 @@ export function ConfirmDelete(props = {}) {
if (props.deleteKey && promises.length) {
Promise.all(promises).finally(() => {
_deleting.delete(props.deleteKey);
if (props.onComplete) props.onComplete();
});
}
} else if (props.deleteKey) {
// Cleanup dimming synchronously on DELETE completion rather than
// relying on a heuristic timeout that breaks when tabs are throttled.
_deleting.delete(props.deleteKey);
}
if (props.onComplete && !props.refresh) {
props.onComplete();
}
} else {
toast(r.error || 'Failed', 'error');
}
+22
View File
@@ -56,6 +56,28 @@ export function parseZones(data) {
* @param {Blob} blob
* @param {string} filename
*/
/**
* Format bytes to human-readable string.
* @param {number} bytes
*/
export function fmtBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(i > 0 ? 1 : 0) + ' ' + sizes[i];
}
/**
* Split a comma-separated string into trimmed, non-empty values.
* @param {string} [value]
* @returns {string[]}
*/
export function csvToArr(value) {
if (!value || !value.trim()) return [];
return value.split(',').map(s => s.trim()).filter(Boolean);
}
export function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
+1 -1
View File
@@ -35,7 +35,7 @@ export { scheduleTokenRefresh, cancelTokenRefresh, checkSession, logout, initAut
export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js';
/* ── Helpers ─────────────────────────────────────────────────── */
export { esc, att_esc, enc, $val, parseZones, downloadBlob } from './helpers.js';
export { esc, att_esc, enc, $val, parseZones, fmtBytes, csvToArr, downloadBlob } from './helpers.js';
/* ── UI Components: Layout ───────────────────────────────────── */
export { PageHeader, renderGuard, renderGuardMulti, Tabs, SectionTitle, ActionGroup, DataTableSection } from './components/layout.js';