Bugzilla – Attachment 192445 Details for
Bug 26355
Allow patrons to self-renew through the OPAC
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 26355: Add a Vue island for the self renewal modal
Bug-26355-Add-a-Vue-island-for-the-self-renewal-mo.patch (text/plain), 16.29 KB, created by
Matt Blenkinsop
on 2026-02-04 10:57:06 UTC
(
hide
)
Description:
Bug 26355: Add a Vue island for the self renewal modal
Filename:
MIME Type:
Creator:
Matt Blenkinsop
Created:
2026-02-04 10:57:06 UTC
Size:
16.29 KB
patch
obsolete
>From ec14e61ba48122f0835b5f5515a2fa26589c80d6 Mon Sep 17 00:00:00 2001 >From: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Date: Wed, 21 Jan 2026 09:43:39 +0000 >Subject: [PATCH] Bug 26355: Add a Vue island for the self renewal modal > >Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk> >--- > .gitignore | 1 + > .../PatronSelfRenewal/PatronSelfRenewal.vue | 171 ++++++++++++++++++ > .../PatronSelfRenewal/VerificationChecks.vue | 85 +++++++++ > .../prog/js/vue/modules/islands.ts | 13 ++ > rspack.config.js | 67 ++++--- > 5 files changed, 309 insertions(+), 28 deletions(-) > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/VerificationChecks.vue > >diff --git a/.gitignore b/.gitignore >index 82402ae9cf7..91557f4995a 100644 >--- a/.gitignore >+++ b/.gitignore >@@ -13,6 +13,7 @@ koha-tmpl/opac-tmpl/bootstrap/css/print.css > koha-tmpl/opac-tmpl/bootstrap/css/print-rtl.css > koha-tmpl/opac-tmpl/bootstrap/css/sco.css > koha-tmpl/opac-tmpl/bootstrap/css/sco-rtl.css >+koha-tmpl/opac-tmpl/bootstrap/js/vue/dist/ > > koha-tmpl/intranet-tmpl/prog/css/maps/ > koha-tmpl/intranet-tmpl/prog/css/bookings.css >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue >new file mode 100644 >index 00000000000..0dd7a48e5c3 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue >@@ -0,0 +1,171 @@ >+<template> >+ <div >+ id="patronSelfRenewal" >+ class="modal modal-full" >+ tabindex="-1" >+ role="dialog" >+ aria-labelledby="patronSelfRenewal" >+ aria-hidden="true" >+ > >+ <div class="modal-dialog modal-xl"> >+ <div v-if="initialized" class="modal-content"> >+ <div class="modal-header"> >+ <h1 class="modal-title"> >+ {{ $__("Patron self-renewal") }} >+ </h1> >+ <button >+ type="button" >+ class="btn-close" >+ data-bs-dismiss="modal" >+ aria-label="Close" >+ ></button> >+ </div> >+ <div class="modal-body"> >+ <VerificationChecks >+ v-if="activeStep === 'verification'" >+ :verificationChecks="verificationChecks" >+ :renewalSettings="renewalSettings" >+ @verification-successful="onVerificationSuccess" >+ /> >+ <VerificationChecks >+ v-if="activeStep === 'confirmation'" >+ :verificationChecks="[ >+ { >+ description: >+ 'Are you sure you want to renew your account?', >+ }, >+ ]" >+ :renewalSettings="renewalSettings" >+ :confirmation="true" >+ @verification-successful="submitRenewal" >+ /> >+ <div v-if="activeStep === 'detailsCheck'"> >+ <legend> >+ {{ $__("Confirm your account details") }} >+ </legend> >+ <div class="detail_confirmation"> >+ <span>{{ >+ $__( >+ "You need to confirm your personal details to proceed with your account renewal." >+ ) >+ }}</span> >+ <button >+ class="btn btn-default" >+ @click="proceedToDetailsVerification()" >+ > >+ {{ $__("Continue") }} >+ </button> >+ </div> >+ </div> >+ </div> >+ <div class="modal-footer"> >+ <button >+ type="button" >+ class="btn btn-default cancel" >+ data-bs-dismiss="modal" >+ > >+ {{ $__("Close") }} >+ </button> >+ </div> >+ </div> >+ </div> >+ </div> >+</template> >+ >+<script> >+import { onBeforeMount, ref } from "vue"; >+import { APIClient } from "../../../fetch/api-client.js"; >+import VerificationChecks from "./VerificationChecks.vue"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ components: { VerificationChecks }, >+ setup(props) { >+ const verificationChecks = ref([]); >+ const initialized = ref(false); >+ const activeStep = ref(null); >+ const renewalSettings = ref({ >+ defaultErrorMessage: $__( >+ "You are not able to self-renew with the provided information. Please visit your library to proceed with your renewal." >+ ), >+ }); >+ >+ onBeforeMount(() => { >+ const client = APIClient.patron; >+ client.self_renewal.start().then( >+ response => { >+ verificationChecks.value = response.verification_checks; >+ renewalSettings.value = { >+ ...renewalSettings.value, >+ ...response.self_renewal_settings, >+ }; >+ activeStep.value = verificationChecks.value.length >+ ? "verification" >+ : response.self_renewal_settings.opac_patron_details === >+ "1" >+ ? "detailsCheck" >+ : "confirmation"; >+ initialized.value = true; >+ }, >+ error => {} >+ ); >+ }); >+ >+ const proceedToDetailsVerification = () => { >+ window.location.href = >+ "/cgi-bin/koha/opac-memberentry.pl?self_renewal=1"; >+ }; >+ >+ const onVerificationSuccess = () => { >+ if (renewalSettings.value.opac_patron_details === "1") { >+ activeStep.value = "detailsCheck"; >+ } else { >+ activeStep.value = "confirmation"; >+ } >+ }; >+ >+ const submitRenewal = () => { >+ const client = APIClient.patron; >+ client.self_renewal.submit({}).then( >+ response => { >+ let newLocation = >+ "/cgi-bin/koha/opac-user.pl?self_renewal_success=" + >+ response.expiry_date; >+ if (response.confirmation_sent) { >+ newLocation += "&confirmation_sent=1"; >+ } >+ document.location = newLocation; >+ }, >+ error => { >+ document.location = >+ "/cgi-bin/koha/opac-user.pl?self_renewal_success=0"; >+ } >+ ); >+ }; >+ >+ return { >+ initialized, >+ activeStep, >+ verificationChecks, >+ renewalSettings, >+ onVerificationSuccess, >+ proceedToDetailsVerification, >+ submitRenewal, >+ }; >+ }, >+}; >+</script> >+ >+<style scoped> >+.detail_confirmation { >+ display: flex; >+ flex-direction: column; >+ gap: 1em; >+} >+.detail_confirmation button { >+ display: flex; >+ flex-direction: column; >+ gap: 1em; >+ width: 7em; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/VerificationChecks.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/VerificationChecks.vue >new file mode 100644 >index 00000000000..e001a210011 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/PatronSelfRenewal/VerificationChecks.vue >@@ -0,0 +1,85 @@ >+<template> >+ <fieldset class="rows" v-if="!verificationFailure"> >+ <legend v-if="!confirmation"> >+ {{ $__("Verification step %s").format(completedCount + 1) }} >+ </legend> >+ <span class="verification_question">{{ activeCheck.description }}</span> >+ <div class="verification_actions"> >+ <button class="btn btn-success" @click="verificationPassed()"> >+ {{ $__("Yes") }} >+ </button> >+ <button class="btn btn-default" @click="verificationFailed()"> >+ {{ $__("No") }} >+ </button> >+ </div> >+ </fieldset> >+ <span class="error" v-else>{{ errorMessage }}</span> >+</template> >+ >+<script> >+import { computed, ref } from "vue"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ props: { >+ verificationChecks: Array, >+ renewalSettings: Object, >+ confirmation: Boolean, >+ }, >+ emits: ["verification-successful"], >+ setup(props, { emit }) { >+ const checkCount = ref(props.verificationChecks.length); >+ const completedCount = ref(0); >+ const verificationFailure = ref(false); >+ >+ const activeCheck = ref(props.verificationChecks[0]); >+ >+ const verificationPassed = () => { >+ if (completedCount.value === checkCount.value - 1) { >+ emit("verification-successful"); >+ } else { >+ completedCount.value++; >+ activeCheck.value = >+ props.verificationChecks[completedCount.value]; >+ } >+ }; >+ const verificationFailed = () => { >+ verificationFailure.value = true; >+ }; >+ >+ const errorMessage = computed(() => { >+ const { self_renewal_failure_message, defaultErrorMessage } = >+ props.renewalSettings; >+ return self_renewal_failure_message || defaultErrorMessage; >+ }); >+ >+ return { >+ checkCount, >+ activeCheck, >+ verificationPassed, >+ verificationFailed, >+ verificationFailure, >+ errorMessage, >+ completedCount, >+ }; >+ }, >+}; >+</script> >+ >+<style scoped> >+.rows { >+ display: flex; >+ flex-direction: column; >+} >+.verification_actions { >+ display: flex; >+ gap: 1em; >+} >+.verification_actions button { >+ width: 5em; >+} >+.verification_question { >+ margin-bottom: 2em; >+ font-size: 100%; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts >index b9b15766ab2..fec9da74428 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts >@@ -72,6 +72,19 @@ export const componentRegistry: Map<string, WebComponentDynamicImport> = > }, > }, > ], >+ [ >+ "patron-self-renewal", >+ { >+ importFn: async () => { >+ const module = await import( >+ /* webpackChunkName: "patron-self-renewal" */ >+ "../components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue" >+ ); >+ return module.default; >+ }, >+ config: {}, >+ }, >+ ], > ]); > > /** >diff --git a/rspack.config.js b/rspack.config.js >index 62cf6b1956c..c9986d99bca 100644 >--- a/rspack.config.js >+++ b/rspack.config.js >@@ -3,8 +3,13 @@ const { VueLoaderPlugin } = require("vue-loader"); > const path = require("path"); > const rspack = require("@rspack/core"); > >-module.exports = [ >- { >+const islandsExport = application => { >+ const vueDir = >+ application === "intranet" >+ ? "intranet-tmpl/prog" >+ : "opac-tmpl/bootstrap"; >+ >+ return { > resolve: { > alias: { > "@fetch": path.resolve( >@@ -15,28 +20,22 @@ module.exports = [ > __dirname, > "koha-tmpl/intranet-tmpl/prog/js/vue" > ), >- "@cypress": path.resolve(__dirname, "t/cypress"), > }, > }, >+ experiments: { >+ outputModule: true, >+ }, > entry: { >- erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts", >- preservation: >- "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts", >- "admin/record_sources": >- "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts", >- acquisitions: >- "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/acquisitions.ts", > islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts", >- sip2: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/sip2.ts", > }, > output: { >- filename: "[name].js", >- path: path.resolve( >- __dirname, >- "koha-tmpl/intranet-tmpl/prog/js/vue/dist/" >- ), >- chunkFilename: "[name].[contenthash].js", >+ filename: "[name].esm.js", >+ path: path.resolve(__dirname, `koha-tmpl/${vueDir}/js/vue/dist/`), >+ chunkFilename: "[name].[contenthash].esm.js", > globalObject: "window", >+ library: { >+ type: "module", >+ }, > }, > module: { > rules: [ >@@ -46,7 +45,7 @@ module.exports = [ > options: { > experimentalInlineMatchResource: true, > }, >- //exclude: [path.resolve(__dirname, "t/cypress/")], >+ exclude: [path.resolve(__dirname, "t/cypress/")], > }, > { > test: /\.ts$/, >@@ -88,7 +87,10 @@ module.exports = [ > "datatables.net-buttons/js/buttons.print": "DataTable", > "datatables.net-buttons/js/buttons.colVis": "DataTable", > }, >- }, >+ }; >+}; >+ >+module.exports = [ > { > resolve: { > alias: { >@@ -96,25 +98,32 @@ module.exports = [ > __dirname, > "koha-tmpl/intranet-tmpl/prog/js/fetch" > ), >+ "@koha-vue": path.resolve( >+ __dirname, >+ "koha-tmpl/intranet-tmpl/prog/js/vue" >+ ), >+ "@cypress": path.resolve(__dirname, "t/cypress"), > }, > }, >- experiments: { >- outputModule: true, >- }, > entry: { >+ erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts", >+ preservation: >+ "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts", >+ "admin/record_sources": >+ "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts", >+ acquisitions: >+ "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/acquisitions.ts", > islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts", >+ sip2: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/sip2.ts", > }, > output: { >- filename: "[name].esm.js", >+ filename: "[name].js", > path: path.resolve( > __dirname, > "koha-tmpl/intranet-tmpl/prog/js/vue/dist/" > ), >- chunkFilename: "[name].[contenthash].esm.js", >+ chunkFilename: "[name].[contenthash].js", > globalObject: "window", >- library: { >- type: "module", >- }, > }, > module: { > rules: [ >@@ -124,7 +133,7 @@ module.exports = [ > options: { > experimentalInlineMatchResource: true, > }, >- exclude: [path.resolve(__dirname, "t/cypress/")], >+ //exclude: [path.resolve(__dirname, "t/cypress/")], > }, > { > test: /\.ts$/, >@@ -167,6 +176,8 @@ module.exports = [ > "datatables.net-buttons/js/buttons.colVis": "DataTable", > }, > }, >+ islandsExport("intranet"), >+ islandsExport("opac"), > { > entry: { > "api-client.cjs": >-- >2.39.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 26355
:
191781
|
191782
|
191783
|
191784
|
191785
|
191786
|
191787
|
191788
|
191789
|
192440
|
192441
|
192442
|
192443
|
192444
| 192445 |
192446
|
192447
|
192448