- asyncBtn now returns fn().finally(...) so callers can await/chain it - Move re-fetch calls inside try/catch in core.js and permissions.js so network errors from fetchAndRenderStalePerms / fetchAndRender* are caught instead of escaping as unhandled rejections - clearStaleAgent returns the asyncBtn promise so the function is properly awaitable when a button is present - Update asyncBtn doc comment to reflect the return-value contract
36 lines
1.6 KiB
JavaScript
36 lines
1.6 KiB
JavaScript
// Shared async-button primitive. Used by both the dashboard and the
|
|
// per-agent UI for any button that triggers a network action.
|
|
//
|
|
// `asyncBtn(btn, fn)` — the single reusable component:
|
|
// 1. Guards double-click: returns immediately if `btn` is already
|
|
// disabled (prevents a second identical request from firing).
|
|
// 2. Saves `btn.innerHTML` and replaces it with a spinner during the
|
|
// async operation.
|
|
// 3. Re-enables the button and restores the original content when `fn`
|
|
// resolves or rejects (via `finally`), so callers don't need
|
|
// save/restore boilerplate.
|
|
//
|
|
// Usage:
|
|
// btn.addEventListener('click', () => asyncBtn(btn, async () => {
|
|
// const resp = await fetch('/api/...');
|
|
// if (!resp.ok) throw new Error(await resp.text());
|
|
// // handle success
|
|
// }));
|
|
//
|
|
// Error handling: `asyncBtn` restores the button on any thrown error /
|
|
// rejected promise but does NOT surface the error — callers must catch
|
|
// and display it themselves (via `themedToast`, `alert`, a status span,
|
|
// etc.) inside `fn` without re-throwing. `fn` must not let errors escape
|
|
// unhandled: `asyncBtn` returns the `fn().finally(...)` promise so
|
|
// callers can optionally chain `.catch` or `await`, but does not add its
|
|
// own catch — an unhandled rejection from `fn` will propagate normally.
|
|
export function asyncBtn(btn, fn) {
|
|
if (btn.disabled) return; // double-click guard
|
|
const orig = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner">◐</span>';
|
|
return fn().finally(() => {
|
|
btn.disabled = false;
|
|
btn.innerHTML = orig;
|
|
});
|
|
}
|