Bugzilla – Attachment 193929 Details for
Bug 41129
Migrate place_booking.js to a Vue island.
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41129: Fix booking modal reopen race and add regression test
Bug-41129-Fix-booking-modal-reopen-race-and-add-re.patch (text/plain), 10.87 KB, created by
Paul Derscheid
on 2026-02-25 17:20:55 UTC
(
hide
)
Description:
Bug 41129: Fix booking modal reopen race and add regression test
Filename:
MIME Type:
Creator:
Paul Derscheid
Created:
2026-02-25 17:20:55 UTC
Size:
10.87 KB
patch
obsolete
>From 282240b7f973674aa55edd863b6d29667835a9a1 Mon Sep 17 00:00:00 2001 >From: Paul Derscheid <paul.derscheid@lmscloud.de> >Date: Wed, 25 Feb 2026 18:14:38 +0100 >Subject: [PATCH] Bug 41129: Fix booking modal reopen race and add regression > test >MIME-Version: 1.0 >Content-Type: text/plain; charset=UTF-8 >Content-Transfer-Encoding: 8bit > >Sponsored-by: Büchereizentrale Schleswig-Holstein <https://www.bz-sh.de/> >--- > .../en/includes/modals/booking/island.inc | 37 ++-- > .../vue/components/Bookings/BookingModal.vue | 1 + > .../Circulation/bookingsModalBasic_spec.ts | 161 ++++++++++++++++++ > 3 files changed, 183 insertions(+), 16 deletions(-) > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/booking/island.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/booking/island.inc >index ed79cb5b300..417aa898a64 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/booking/island.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/booking/island.inc >@@ -10,6 +10,10 @@ > hydrate(); > > const island = document.querySelector("booking-modal-island"); >+ const islandReady = (async () => { >+ await customElements.whenDefined("booking-modal-island"); >+ return document.querySelector("booking-modal-island"); >+ })(); > > const parseJson = value => { > if (!value) return []; >@@ -21,8 +25,8 @@ > } > }; > >- const normalizeProps = source => { >- if (!island || !source) return; >+ const normalizeProps = (targetIsland, source) => { >+ if (!targetIsland || !source) return; > > const bookingId = source.booking ?? source.bookingId ?? null; > const itemId = source.itemnumber ?? source.itemId ?? null; >@@ -36,32 +40,33 @@ > const biblionumber = > source.biblionumber ?? source.biblio_id ?? source.biblioId; > >- island.bookingId = bookingId; >- island.itemId = itemId; >- island.patronId = patronId; >- island.pickupLibraryId = pickupLibraryId; >- island.startDate = startDate; >- island.endDate = endDate; >- island.itemtypeId = itemtypeId; >- island.selectedDateRange = startDate >+ targetIsland.bookingId = bookingId; >+ targetIsland.itemId = itemId; >+ targetIsland.patronId = patronId; >+ targetIsland.pickupLibraryId = pickupLibraryId; >+ targetIsland.startDate = startDate; >+ targetIsland.endDate = endDate; >+ targetIsland.itemtypeId = itemtypeId; >+ targetIsland.selectedDateRange = startDate > ? [startDate, endDate || startDate] > : []; > > const extendedAttributes = > source.extendedAttributes ?? source.extended_attributes ?? []; >- island.extendedAttributes = Array.isArray(extendedAttributes) >+ targetIsland.extendedAttributes = Array.isArray(extendedAttributes) > ? extendedAttributes > : parseJson(extendedAttributes); > > if (biblionumber) { >- island.biblionumber = biblionumber; >+ targetIsland.biblionumber = biblionumber; > } > }; > >- const openModal = props => { >- if (!island) return; >- normalizeProps(props || {}); >- island.open = true; >+ const openModal = async props => { >+ const targetIsland = await islandReady; >+ if (!targetIsland) return; >+ normalizeProps(targetIsland, props || {}); >+ targetIsland.open = true; > }; > > if (island) { >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue >index fa1770320c7..5e50fcfbf3d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue >@@ -770,6 +770,7 @@ onMounted(() => { > keyboard: false, > }); > modalElement.value.addEventListener("hidden.bs.modal", () => { >+ modalState.isOpen = false; > emit("close"); > resetModalState(); > }); >diff --git a/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts b/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >index a446c3e1f5c..5d2909300e0 100644 >--- a/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >+++ b/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >@@ -767,6 +767,167 @@ describe("Booking Modal Basic Tests", () => { > }); > }); > >+ it("should refresh edit modal state across consecutive openings", () => { >+ const today = dayjs().startOf("day"); >+ let secondPatron; >+ let firstBookingId; >+ let secondBookingId; >+ >+ const firstBooking = { >+ start: today.add(8, "day"), >+ end: today.add(10, "day"), >+ patron_id: testData.patron.patron_id, >+ patron_label: testData.patron.surname, >+ }; >+ >+ cy.task("buildSampleObject", { >+ object: "patron", >+ values: { >+ firstname: "Jane", >+ surname: "Roe", >+ cardnumber: `TEST${Date.now()}B`, >+ category_id: "PT", >+ library_id: testData.libraries[0].library_id, >+ }, >+ }).then(mockPatron => { >+ secondPatron = mockPatron; >+ >+ return cy.task("query", { >+ sql: `INSERT INTO borrowers (borrowernumber, firstname, surname, cardnumber, categorycode, branchcode, dateofbirth) >+ VALUES (?, ?, ?, ?, ?, ?, ?)`, >+ values: [ >+ mockPatron.patron_id, >+ mockPatron.firstname, >+ mockPatron.surname, >+ mockPatron.cardnumber, >+ mockPatron.category_id, >+ mockPatron.library_id, >+ "1990-01-01", >+ ], >+ }); >+ }); >+ >+ cy.then(() => >+ cy >+ .task("query", { >+ sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status) >+ VALUES (?, ?, ?, ?, ?, ?, '1')`, >+ values: [ >+ testData.biblio.biblio_id, >+ testData.items[0].item_id, >+ firstBooking.patron_id, >+ firstBooking.start.format("YYYY-MM-DD HH:mm:ss"), >+ firstBooking.end.format("YYYY-MM-DD HH:mm:ss"), >+ testData.libraries[0].library_id, >+ ], >+ }) >+ .then(result => { >+ firstBookingId = result.insertId; >+ }) >+ ); >+ >+ cy.then(() => { >+ const secondBooking = { >+ start: today.add(15, "day"), >+ end: today.add(17, "day"), >+ patron_id: secondPatron.patron_id, >+ patron_label: secondPatron.surname, >+ }; >+ >+ return cy >+ .task("query", { >+ sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status) >+ VALUES (?, ?, ?, ?, ?, ?, '1')`, >+ values: [ >+ testData.biblio.biblio_id, >+ testData.items[1].item_id, >+ secondBooking.patron_id, >+ secondBooking.start.format("YYYY-MM-DD HH:mm:ss"), >+ secondBooking.end.format("YYYY-MM-DD HH:mm:ss"), >+ testData.libraries[0].library_id, >+ ], >+ }) >+ .then(result => { >+ secondBookingId = result.insertId; >+ }) >+ .then(() => secondBooking); >+ }).then(secondBooking => { >+ cy.visit( >+ `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` >+ ); >+ cy.get("booking-modal-island .modal").should("exist"); >+ >+ // First open: booking A >+ cy.window().then(win => { >+ win.openBookingModal({ >+ booking: String(firstBookingId), >+ patron: String(firstBooking.patron_id), >+ itemnumber: String(testData.items[0].item_id), >+ pickup_library: testData.libraries[0].library_id, >+ start_date: firstBooking.start.startOf("day").toISOString(), >+ end_date: firstBooking.end.endOf("day").toISOString(), >+ biblionumber: String(testData.biblio.biblio_id), >+ }); >+ }); >+ >+ cy.get("booking-modal-island .modal", { timeout: 10000 }).should( >+ "be.visible" >+ ); >+ cy.vueSelectShouldHaveValue( >+ "booking_patron", >+ firstBooking.patron_label >+ ); >+ >+ cy.get("booking-modal-island .modal .btn-close").first().click(); >+ cy.get("booking-modal-island .modal").should("not.be.visible"); >+ cy.get("body").should("not.have.class", "modal-open"); >+ >+ // Second open: booking B (must not stay stale with booking A data) >+ cy.window().then(win => { >+ win.openBookingModal({ >+ booking: String(secondBookingId), >+ patron: String(secondBooking.patron_id), >+ itemnumber: String(testData.items[1].item_id), >+ pickup_library: testData.libraries[0].library_id, >+ start_date: secondBooking.start >+ .startOf("day") >+ .toISOString(), >+ end_date: secondBooking.end.endOf("day").toISOString(), >+ biblionumber: String(testData.biblio.biblio_id), >+ }); >+ }); >+ >+ cy.get("booking-modal-island .modal", { timeout: 10000 }).should( >+ "be.visible" >+ ); >+ cy.vueSelectShouldHaveValue( >+ "booking_patron", >+ secondBooking.patron_label >+ ); >+ }); >+ >+ cy.then(() => { >+ if (firstBookingId) { >+ cy.task("query", { >+ sql: "DELETE FROM bookings WHERE booking_id = ?", >+ values: [firstBookingId], >+ }); >+ } >+ if (secondBookingId) { >+ cy.task("query", { >+ sql: "DELETE FROM bookings WHERE booking_id = ?", >+ values: [secondBookingId], >+ }); >+ } >+ if (secondPatron) { >+ cy.task("query", { >+ sql: "DELETE FROM borrowers WHERE borrowernumber = ?", >+ values: [secondPatron.patron_id], >+ }); >+ } >+ }); >+ }); >+ > it("should handle booking failure gracefully", () => { > /** > * Comprehensive Error Handling and Recovery Test >-- >2.53.0
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 41129
:
188546
|
188551
|
189348
|
189350
|
193909
|
193910
|
193911
|
193912
|
193913
|
193925
|
193926
|
193927
|
193928
| 193929