Bug 15792: Throttle patron renewal links to prevent double-clicks

- Replace plain <a> renewal links in patron_messages.inc with the
  reusable throttled-button component introduced in Bug 40896
- Covers all four renewal links: warndeparture and expired states,
  in both circulation and patron detail contexts
- Include throttledButton.js in circulation.tt and moremember.tt
- Add persist mode and done icon state to throttled-button component
  so the button stays disabled on bfcache restore with a check icon
  instead of a misleading spinner

Test plan:
1) Create or update a patron category with an enrollment fee and an
   enrollment period in months (e.g. 24 months)
2) Create a patron of that type and write off any initial enrollment
   fee
3) Set the patron's dateexpiry to the past so the account is expired
4) Go to /cgi-bin/koha/circ/circulation.pl and scan the patron
5) Observe the "Renew" link in the expiration warning now shows as a
   small button with a rotate icon
6) Click "Renew" and confirm the button becomes disabled with a
   spinner, preventing a second click
7) Verify the patron is renewed correctly with only one enrollment
   fee charged and the correct expiry date
8) Press the browser back button to return via bfcache
9) Verify the button remains disabled and shows a check icon instead
   of the spinner, indicating the action was already completed
10) Repeat from the patron detail page (moremember.pl) to verify the
    same throttle and done-state behavior there
11) Test the warndeparture case by setting dateexpiry to a near-future
    date and confirming the same behavior

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Paul Derscheid 2026-03-03 10:08:05 +01:00 committed by Lucas Gass
parent 96d2287a6a
commit 3a6a79a607
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
5 changed files with 31 additions and 6 deletions

View File

@ -24,10 +24,12 @@
<span class="circ-hlt">Expiration:</span>
<span>Patron's card expires on [% patron.dateexpiry | $KohaDates %].</span>
[% IF ( moremember ) %]
<a href="/cgi-bin/koha/members/setstatus.pl?borrowernumber=[% patron.borrowernumber | uri %]&amp;destination=member&amp;reregistration=y">Renew</a> or
[% INCLUDE 'throttled-button.inc' element_type='link' href='/cgi-bin/koha/members/setstatus.pl?borrowernumber=' _ patron.borrowernumber _ '&destination=member&reregistration=y' classes='btn btn-default btn-xs' ready_icon='fa-solid fa-rotate' label=t('Renew') persist=1 done_icon='fa-solid fa-check' %]
or
<a href="/cgi-bin/koha/members/memberentry.pl?op=edit_form&amp;destination=member&amp;borrowernumber=[% patron.borrowernumber | uri %]">Edit details</a>
[% ELSE %]
<a href="/cgi-bin/koha/members/setstatus.pl?borrowernumber=[% patron.borrowernumber | uri %]&amp;destination=circ&amp;reregistration=y">Renew</a> or
[% INCLUDE 'throttled-button.inc' element_type='link' href='/cgi-bin/koha/members/setstatus.pl?borrowernumber=' _ patron.borrowernumber _ '&destination=circ&reregistration=y' classes='btn btn-default btn-xs' ready_icon='fa-solid fa-rotate' label=t('Renew') persist=1 done_icon='fa-solid fa-check' %]
or
<a href="/cgi-bin/koha/members/memberentry.pl?op=edit_form&amp;destination=circ&amp;borrowernumber=[% patron.borrowernumber | uri %]">Edit details</a>
[% END %]
</li>
@ -47,10 +49,12 @@
<span>Patron's card expired on [% expiry | $KohaDates %]</span>
[% END %]
[% IF ( moremember ) %]
<a href="/cgi-bin/koha/members/setstatus.pl?borrowernumber=[% patron.borrowernumber | uri %]&amp;destination=member&amp;reregistration=y">Renew</a> or
[% INCLUDE 'throttled-button.inc' element_type='link' href='/cgi-bin/koha/members/setstatus.pl?borrowernumber=' _ patron.borrowernumber _ '&destination=member&reregistration=y' classes='btn btn-default btn-xs' ready_icon='fa-solid fa-rotate' label=t('Renew') persist=1 done_icon='fa-solid fa-check' %]
or
<a href="/cgi-bin/koha/members/memberentry.pl?op=edit_form&amp;destination=member&amp;borrowernumber=[% patron.borrowernumber | uri %]">Edit details</a>
[% ELSE %]
<a href="/cgi-bin/koha/members/setstatus.pl?borrowernumber=[% patron.borrowernumber | uri %]&amp;destination=circ&amp;reregistration=y">Renew</a> or
[% INCLUDE 'throttled-button.inc' element_type='link' href='/cgi-bin/koha/members/setstatus.pl?borrowernumber=' _ patron.borrowernumber _ '&destination=circ&reregistration=y' classes='btn btn-default btn-xs' ready_icon='fa-solid fa-rotate' label=t('Renew') persist=1 done_icon='fa-solid fa-check' %]
or
<a href="/cgi-bin/koha/members/memberentry.pl?op=edit_form&amp;destination=circ&amp;borrowernumber=[% patron.borrowernumber | uri %]">Edit details</a>
[% END %]
</li>

View File

@ -7,12 +7,16 @@
[% DEFAULT href = '#' %]
[%# Common attributes shared by both button and link %]
[% BLOCK common_attrs %]class="[% classes | html %]"[% IF id %]id="[% id | html %]"[% END %] data-state="ready" data-throttled-button [% IF attrs %][% FOREACH key IN attrs.keys %][% key | html %]="[% attrs.$key | html %]"[% END %][% END %][% END %]
[% BLOCK common_attrs %]
class="[% classes | html %]"[% IF id %]id="[% id | html %]"[% END %] data-state="ready" data-throttled-button [% IF persist %]data-throttle-persist[% END %]
[% IF attrs %][% FOREACH key IN attrs.keys %][% key | html %]="[% attrs.$key | html %]"[% END %][% END %]
[% END %]
[%# Inner content shared by both button and link %]
[% BLOCK inner_content %]
<span class="button-icon-ready"><i class="[% ready_icon | html %]"></i></span>
<span class="button-icon-running" style="display: none;"><i class="[% active_icon | html %]"></i></span>
[% IF done_icon %]<span class="button-icon-done" style="display: none;"><i class="[% done_icon | html %]"></i></span>[% END %]
[% UNLESS icon_only %][% label | html %][% END %]
[% END %]

View File

@ -1260,6 +1260,7 @@
[% Asset.js("js/checkouts.js") | $raw %]
[% Asset.js("js/tables/bookings.js") | $raw %]
[% Asset.js("js/recalls.js") | $raw %]
[% Asset.js("js/throttledButton.js") | $raw %]
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -849,6 +849,7 @@
[% INCLUDE 'str/members-menu.inc' %]
[% Asset.js("js/members-menu.js") | $raw %]
[% Asset.js("js/recalls.js") | $raw %]
[% Asset.js("js/throttledButton.js") | $raw %]
[% Asset.js("js/hold-group.js") | $raw %]
<script nonce="[% Koha.CSPNonce | $raw %]">
const LoadCheckoutsTableDelay = 0;

View File

@ -74,7 +74,22 @@
clearTimeout(timeoutId);
pendingTimeouts.delete(button);
}
toggleButton(button);
if (!button.hasAttribute("data-throttle-persist")) {
toggleButton(button);
} else {
const runningIcon = button.querySelector(
".button-icon-running"
);
const doneIcon =
button.querySelector(".button-icon-done");
if (runningIcon) {
runningIcon.style.display = "none";
}
if (doneIcon) {
doneIcon.style.display = "";
}
button.dataset.state = "done";
}
});
}
});