From 1acc271108a7b6b2ff31ec98e5eeb2fcfea50ae7 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 20 Jul 2026 19:59:45 +0200 Subject: [PATCH] fix(#2443): address asyncBtn review notes - 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 --- frontend/packages/dashboard/src/core.js | 3 +-- frontend/packages/dashboard/src/permissions.js | 10 +++++----- frontend/packages/shared/src/forms.js | 8 +++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/packages/dashboard/src/core.js b/frontend/packages/dashboard/src/core.js index 61919464..75059976 100644 --- a/frontend/packages/dashboard/src/core.js +++ b/frontend/packages/dashboard/src/core.js @@ -115,12 +115,11 @@ function renderStalePerms(root, ghosts) { errP.hidden = false; return; } + await fetchAndRenderStalePerms(); } catch (err) { errP.textContent = 'failed to clear perms for ' + name + ': ' + err; errP.hidden = false; - return; } - await fetchAndRenderStalePerms(); })); li.append(btn); ul.append(li); diff --git a/frontend/packages/dashboard/src/permissions.js b/frontend/packages/dashboard/src/permissions.js index 008c1625..02f6eb10 100644 --- a/frontend/packages/dashboard/src/permissions.js +++ b/frontend/packages/dashboard/src/permissions.js @@ -337,17 +337,17 @@ async function clearStaleAgent(name, sectionRoot) { setSaveNote('failed to remove ' + name + ': ' + text, true); return; } + // Re-fetch both sections so the stale row disappears. + await Promise.all([fetchAndRenderCapabilities(), fetchAndRenderToolGroups()]); } catch (err) { setSaveNote('failed to remove ' + name + ': ' + err, true); - return; } - // Re-fetch both sections so the stale row disappears. - await Promise.all([fetchAndRenderCapabilities(), fetchAndRenderToolGroups()]); }; // asyncBtn guards double-submit; fall through without guard when there // is no button (e.g. called programmatically without a DOM context). - if (btn) asyncBtn(btn, doDelete); - else await doDelete(); + // Return the promise so callers can await clearStaleAgent() if needed. + if (btn) return asyncBtn(btn, doDelete); + await doDelete(); } function clearSaveStatus() { diff --git a/frontend/packages/shared/src/forms.js b/frontend/packages/shared/src/forms.js index 6d59f66c..a68cf694 100644 --- a/frontend/packages/shared/src/forms.js +++ b/frontend/packages/shared/src/forms.js @@ -20,14 +20,16 @@ // 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.) before the re-throw, or handle it inside `fn` without -// re-throwing. +// 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 = ''; - fn().finally(() => { + return fn().finally(() => { btn.disabled = false; btn.innerHTML = orig; });