fix: gate feedback dialog Cancel/X on an in-flight submit

Escape and backdrop clicks were already ignored while the POST was
pending, but the X, Close, and Cancel buttons weren't — clicking one
mid-send settled the promise false and tore down the overlay while the
submission still landed server-side: the developer got the email, the
app was told sent=false, and a user who resubmitted 'the failed one'
sent a duplicate and burned a daily-cap slot.

Apply the same !sending gate to the buttons and disable them visually
while the send is in flight.
This commit is contained in:
Nariman Jelveh
2026-08-11 18:26:47 -07:00
parent b1fdc8d665
commit 1c2d33f149
2 changed files with 17 additions and 3 deletions
+9 -1
View File
@@ -142,7 +142,13 @@ async function UIWindowAppFeedback (options) {
$overlay.on('mousedown', (e) => {
if ( e.target === $overlay.get(0) && ! sending ) close();
});
$overlay.find('.app-feedback-x, .app-feedback-close-btn, .app-feedback-cancel-btn').on('click', close);
// Same in-flight gate as Escape/backdrop: closing while the POST is
// pending would settle false for a submission that still lands
// server-side — the developer gets the email while the app is told
// nothing was sent.
$overlay.find('.app-feedback-x, .app-feedback-close-btn, .app-feedback-cancel-btn').on('click', () => {
if ( ! sending ) close();
});
$overlay.find('.app-feedback-message').on('input', function () {
$overlay.find('.app-feedback-counter').text(`${this.value.length} / ${MESSAGE_MAX_LENGTH}`);
@@ -155,6 +161,7 @@ async function UIWindowAppFeedback (options) {
if ( ! message || sending ) return;
sending = true;
$btn.prop('disabled', true);
$overlay.find('.app-feedback-x, .app-feedback-cancel-btn').prop('disabled', true);
$overlay.find('.app-feedback-error').hide();
try {
const res = await fetch(`${window.api_origin}/app-feedback`, {
@@ -193,6 +200,7 @@ async function UIWindowAppFeedback (options) {
$overlay.find('.app-feedback-error').text(i18n('app_feedback_error')).show();
} finally {
sending = false;
$overlay.find('.app-feedback-x, .app-feedback-cancel-btn').prop('disabled', false);
$overlay.find('.app-feedback-send-btn').prop('disabled',
settled || String($overlay.find('.app-feedback-message').val() || '').trim() === '');
}
+8 -2
View File
@@ -1619,7 +1619,7 @@ input.myapps-group-name:focus {
height: 16px;
}
@media (hover: hover) {
.app-feedback-x:hover {
.app-feedback-x:hover:not(:disabled) {
background: var(--afb-hover);
color: var(--afb-fg);
}
@@ -1727,7 +1727,7 @@ input.myapps-group-name:focus {
transition: background-color 0.12s ease, opacity 0.12s ease, filter 0.12s ease;
}
@media (hover: hover) {
.app-feedback-btn:hover {
.app-feedback-btn:hover:not(:disabled) {
background: var(--afb-hover);
}
}
@@ -1747,6 +1747,12 @@ input.myapps-group-name:focus {
cursor: default;
filter: none;
}
/* Cancel/X are disabled while a submit is in flight. */
.app-feedback-btn:disabled,
.app-feedback-x:disabled {
opacity: 0.5;
cursor: default;
}
.app-feedback-success {
text-align: center;