Bug 42732: Migrate catalogue/detail.tt addToBundle $.post to api-client.js

Replace direct $.post() to /api/v1/items/{id}/bundled_items with
ItemAPIClient.bundled_items.add(), which uses http-client.js and
automatically includes the CSRF token header.

The caller passes return_response: true because it needs access to
the raw response status code and body for fine-grained error handling
(different user-facing messages for 409/already_bundled, 409/checked_out,
409/reserved, 404/not_found, 400/failed_nesting). http-client.js
default behavior throws a plain Error on non-2xx responses, losing
the status code and error_code fields the UI needs.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Tomás Cohen Arazi 2026-06-01 13:58:51 -03:00 committed by Pedro Amorim
parent e2b0883811
commit 058c476bc7
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
3 changed files with 63 additions and 53 deletions

View File

@ -2018,65 +2018,64 @@
bundle_form_active = item_id;
});
function addToBundle(url, data) {
function addToBundle(item_id, data) {
/* Send the data using post with external_id */
var posting = $.post({
url: url,
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
});
const client = APIClient.item;
const posting = client.bundled_items.add(item_id, data, { return_response: true });
const barcode = data.external_id;
const marc_link = data.marc_link;
/* Report the results */
posting.done(function (data) {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-success">' + _("Success: Added '%s'").format(barcode) + "</div>");
$("#external_id").val("").focus();
bundle_changed = 1;
});
posting.fail(function (data) {
if (data.status === 409) {
var response = data.responseJSON;
if (response.error_code === "already_bundled") {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-warning">' + _("Warning: Item '%s' already attached").format(barcode) + "</div>");
} else if (response.error_code === "bundle_checkout_out") {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Bundle is currently checked out") + "</div>");
} else if (response.error_code === "checked_out") {
const button = $('<button type="button">')
.addClass("btn btn-xs")
.text(_("Check in and add to bundle"))
.on("click", function () {
addToBundle(url, { external_id: barcode, force_checkin: true, marc_link: marc_link });
});
$("#addResult").empty().attr("class", "alert alert-warning").append(__x("Warning: Item {barcode} is checked out", { barcode })).append(" ", button);
} else if (response.error_code === "failed_checkin") {
$("#addResult").empty().attr("class", "alert alert-danger").append(__x("Failure: Item {barcode} cannot be checked in", { barcode }));
} else if (response.error_code === "reserved") {
const button = $('<button type="button">')
.addClass("btn btn-xs")
.text(_("Ignore holds and add to bundle"))
.on("click", function () {
addToBundle(url, { external_id: barcode, ignore_holds: true, marc_link: marc_link });
});
$("#addResult").empty().attr("class", "alert alert-warning").append(__x("Warning: Item {barcode} is on hold", { barcode })).append(" ", button);
} else {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Item '%s' belongs to another bundle").format(barcode) + "</div>");
}
} else if (data.status === 404) {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Item '%s' not found").format(barcode) + "</div>");
} else if (data.status === 400) {
var response = data.responseJSON;
if (response.error_code === "failed_nesting") {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Item '%s' is a bundle and bundles cannot be nested").format(barcode) + "</div>");
} else {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Check the logs for details.") + "</div>");
}
posting.then(function (response) {
if (response.ok) {
response.json().then(function () {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-success">' + _("Success: Added '%s'").format(barcode) + "</div>");
$("#external_id").val("").focus();
bundle_changed = 1;
});
} else {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Check the logs for details.") + "</div>");
response.json().then(function (responseJSON) {
if (response.status === 409) {
if (responseJSON.error_code === "already_bundled") {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-warning">' + _("Warning: Item '%s' already attached").format(barcode) + "</div>");
} else if (responseJSON.error_code === "bundle_checkout_out") {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Bundle is currently checked out") + "</div>");
} else if (responseJSON.error_code === "checked_out") {
const button = $('<button type="button">')
.addClass("btn btn-xs")
.text(_("Check in and add to bundle"))
.on("click", function () {
addToBundle(item_id, { external_id: barcode, force_checkin: true, marc_link: marc_link });
});
$("#addResult").empty().attr("class", "alert alert-warning").append(__x("Warning: Item {barcode} is checked out", { barcode })).append(" ", button);
} else if (responseJSON.error_code === "failed_checkin") {
$("#addResult").empty().attr("class", "alert alert-danger").append(__x("Failure: Item {barcode} cannot be checked in", { barcode }));
} else if (responseJSON.error_code === "reserved") {
const button = $('<button type="button">')
.addClass("btn btn-xs")
.text(_("Ignore holds and add to bundle"))
.on("click", function () {
addToBundle(item_id, { external_id: barcode, ignore_holds: true, marc_link: marc_link });
});
$("#addResult").empty().attr("class", "alert alert-warning").append(__x("Warning: Item {barcode} is on hold", { barcode })).append(" ", button);
} else {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Item '%s' belongs to another bundle").format(barcode) + "</div>");
}
} else if (response.status === 404) {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Item '%s' not found").format(barcode) + "</div>");
} else if (response.status === 400) {
if (responseJSON.error_code === "failed_nesting") {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Item '%s' is a bundle and bundles cannot be nested").format(barcode) + "</div>");
} else {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Check the logs for details.") + "</div>");
}
} else {
$("#addResult").replaceWith('<div id="addResult" class="alert alert-danger">' + _("Failure: Check the logs for details.") + "</div>");
}
$("#external_id").val("").focus();
});
}
$("#external_id").val("").focus();
});
}
@ -2084,10 +2083,9 @@
/* stop form from submitting normally */
event.preventDefault();
const url = this.action;
const data = { external_id: this.elements.external_id.value, marc_link: this.elements.bundle_link.checked };
addToBundle(url, data);
addToBundle(bundle_form_active, data);
});
$("#addToBundleModal").on("hidden.bs.modal", function (e) {

View File

@ -194,6 +194,7 @@ export const APIClient = {
club: createClientProxy(() => import("./club-api-client.js")),
cover_image: createClientProxy(() => import("./cover-image-api-client.js")),
ill: createClientProxy(() => import("./ill-api-client.js")),
item: createClientProxy(() => import("./item-api-client.js")),
localization: createClientProxy(
() => import("./localization-api-client.js")
),

View File

@ -24,6 +24,17 @@ export class ItemAPIClient {
};
}
get bundled_items() {
return {
add: (item_id, body, params = {}) =>
this.httpClient.post({
endpoint: "items/" + item_id + "/bundled_items",
body,
...params,
}),
};
}
get item_types() {
return {
getAll: (query, params, headers) =>