From 461bd5f689f2991611f48d6945e1ce7fe2ed6518 Mon Sep 17 00:00:00 2001 From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> Date: Tue, 25 Apr 2023 12:12:01 +0200 Subject: [PATCH] Bug 30708: Set default values for items added in batch to a train Content-Type: text/plain; charset=utf-8 Technical notes: Ideally we would have split TrainsFormAddItem to make some part reusable, but it turned out into a complicated component that would have been hard to maintain. It seems easier to have two different components. Ideas to improve this area are welcome! Sponsored-by: BULAC - http://www.bulac.fr/ Signed-off-by: BULAC - http://www.bulac.fr/ Signed-off-by: Heather Hernandez <heather_hernandez@nps.gov> Signed-off-by: Laurence Rault <laurence.rault@biblibre.com> Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> --- Koha/Preservation/Train.pm | 4 +- .../Preservation/TrainsFormAddItems.vue | 285 ++++++++++++++++++ .../components/Preservation/WaitingList.vue | 41 +-- .../prog/js/vue/routes/preservation.js | 12 + t/cypress/integration/Preservation/Trains.ts | 60 ++++ .../integration/Preservation/WaitingList.ts | 46 --- 6 files changed, 367 insertions(+), 81 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue diff --git a/Koha/Preservation/Train.pm b/Koha/Preservation/Train.pm index 7ecd7908b4..46abf54682 100644 --- a/Koha/Preservation/Train.pm +++ b/Koha/Preservation/Train.pm @@ -120,7 +120,9 @@ sub add_items { my @added_items; for my $train_item (@$train_items) { try { - push @added_items, $self->add_item($train_item); + my $added_item = $self->add_item($train_item); + $added_item->attributes($train_item->{attributes}); + push @added_items, $added_item; } catch { # FIXME Do we rollback and raise an error or just skip it? diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue new file mode 100644 index 0000000000..814ab7eb9b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue @@ -0,0 +1,285 @@ +<template> + <div v-if="!initialized">{{ $__("Loading") }}</div> + <div v-else id="trains_add_items"> + <h2>{{ $__("Add new items to %s").format(train.name) }}</h2> + <form @submit="onSubmit($event)"> + <fieldset class="rows"> + <ol> + <li> + <label for="itemnumbers" + >{{ $__("Itemnumbers") }}:</label + > + <span>{{ items.map(i => i.item_id).join(", ") }}</span> + </li> + <li> + <label for="processing" + >{{ $__("Processing") }}: + </label> + <v-select + id="processing" + label="name" + v-model="processing_id" + @option:selected="refreshAttributes(1)" + :reduce="p => p.processing_id" + :options="processings" + :clearable="false" + /> + </li> + <li + class="attribute" + v-for="(attribute, counter) in attributes" + v-bind:key="counter" + > + <label :for="`attribute_${counter}`" + >{{ attribute.name }}: + </label> + <span v-if="attribute.type == 'authorised_value'"> + <v-select + :id="`attribute_${counter}`" + v-model="attribute.value" + label="description" + :reduce="av => av.value" + :options="av_options[attribute.option_source]" + taggable + :create-option=" + attribute => ({ + value: attribute, + description: attribute, + }) + " + /> + </span> + <span v-else-if="attribute.type == 'free_text'"> + <input + :id="`attribute_${counter}`" + v-model="attribute.value" + /> + </span> + <span v-else-if="attribute.type == 'db_column'"> + {{ + $__( + "Cannot be edited now, the value will be retrieved from %s" + ).format(attribute.option_source) + }} + </span> + <a + v-if=" + attribute.type != 'db_column' && + (attributes.length == counter + 1 || + attributes[counter + 1] + .processing_attribute_id != + attribute.processing_attribute_id) + " + class="btn btn-link" + @click=" + addAttribute(attribute.processing_attribute_id) + " + ><font-awesome-icon icon="plus" /> + {{ $__("Add") }}</a + > + <a + v-else-if="attribute.type != 'db_column'" + class="btn btn-link" + @click="removeAttribute(counter)" + ><font-awesome-icon icon="minus" /> + {{ $__("Remove") }}</a + > + </li> + </ol> + </fieldset> + <fieldset class="action"> + <input type="submit" value="Submit" /> + <router-link + to="/cgi-bin/koha/preservation/trains" + role="button" + class="cancel" + >{{ $__("Cancel") }}</router-link + > + </fieldset> + </form> + </div> +</template> + +<script> +import { inject } from "vue" +import { APIClient } from "../../fetch/api-client" + +export default { + setup() { + const { setMessage, setWarning, loading, loaded } = inject("mainStore") + return { + setMessage, + setWarning, + loading, + loaded, + api_mappings, + } + }, + data() { + return { + train: { + train_id: null, + name: "", + description: "", + }, + items: [], + train_items: [], + processings: [], + processing: null, + processing_id: null, + initialized: false, + av_options: {}, + attributes: [], + } + }, + beforeCreate() { + const client = APIClient.preservation + client.processings + .getAll() + .then(processings => (this.processings = processings)) + }, + beforeRouteEnter(to, from, next) { + next(vm => { + vm.train = vm + .getTrain(to.params.train_id) + .then(() => + vm + .getItems(to.params.item_ids.split(",")) + .then(() => + vm + .refreshAttributes() + .then(() => (vm.initialized = true)) + ) + ) + }) + }, + methods: { + async getTrain(train_id) { + const client = APIClient.preservation + await client.trains.get(train_id).then( + train => { + this.train = train + this.processing_id = train.default_processing_id + }, + error => {} + ) + }, + async getItems(item_ids) { + const client = APIClient.item + let q = { "me.item_id": item_ids } + await client.items + .getAll(q, { headers: { "x-koha-embed": "biblio" } }) + .then( + items => { + this.items = items + }, + error => {} + ) + }, + columnApiMapping(item, db_column) { + let table_col = db_column.split(".") + let table = table_col[0] + let col = table_col[1] + let api_attribute = this.api_mappings[table][col] || col + return table == "biblio" || table == "biblioitems" + ? item.biblio[api_attribute] + : item[api_attribute] + }, + async refreshAttributes() { + this.loading() + + const client = APIClient.preservation + await client.processings.get(this.processing_id).then( + processing => (this.processing = processing), + error => {} + ) + this.attributes = [] + this.processing.attributes.forEach(attribute => { + this.attributes.push({ + processing_attribute_id: attribute.processing_attribute_id, + name: attribute.name, + type: attribute.type, + option_source: attribute.option_source, + value: "", + }) + }) + const client_av = APIClient.authorised_values + let av_cat_array = this.processing.attributes + .filter(attribute => attribute.type == "authorised_value") + .map(attribute => attribute.option_source) + + client_av.values + .getCategoriesWithValues([ + ...new Set(av_cat_array.map(av_cat => '"' + av_cat + '"')), + ]) // unique + .then(av_categories => { + av_cat_array.forEach(av_cat => { + let av_match = av_categories.find( + element => element.category_name == av_cat + ) + this.av_options[av_cat] = av_match.authorised_values + }) + }) + .then(() => this.loaded()) + }, + addAttribute(processing_attribute_id) { + let last_index = this.attributes.findLastIndex( + attribute => + attribute.processing_attribute_id == processing_attribute_id + ) + let new_attribute = (({ value, ...keepAttrs }) => keepAttrs)( + this.attributes[last_index] + ) + this.attributes.splice(last_index + 1, 0, new_attribute) + }, + removeAttribute(counter) { + this.attributes.splice(counter, 1) + }, + onSubmit(e) { + e.preventDefault() + + let train_items = this.items.map(item => { + return { + item_id: item.item_id, + processing_id: this.processing_id, + attributes: this.attributes.map(a => { + let value = + a.type == "db_column" + ? this.columnApiMapping(item, a.option_source) + : a.value + return { + processing_attribute_id: a.processing_attribute_id, + value, + } + }), + } + }) + + const client = APIClient.preservation + client.train_items.createAll(train_items, this.train.train_id).then( + result => { + if (result.length) { + this.setMessage( + this.$__( + "%s items have been added to train %s." + ).format(result.length, this.train.train_id) + ) + + this.$router.push( + "/cgi-bin/koha/preservation/trains/" + + this.train.train_id + ) + } else { + this.setMessage( + this.$__("No items have been added to the train.") + ) + } + }, + error => {} + ) + }, + }, + components: {}, + name: "TrainsFormAddItems", +} +</script> diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue index a1d627ecd5..a290ca5615 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue @@ -189,40 +189,13 @@ export default { }, addItemsToTrain: function (e) { e.preventDefault() - this.loading() - let item_ids = Object.values(this.last_items) - const client = APIClient.preservation - client.train_items - .createAll(item_ids, this.train_id_selected_for_add) - .then( - result => { - if (result.length) { - this.setMessage( - this.$__( - "%s items have been added to train %s." - ).format( - result.length, - this.train_id_selected_for_add - ), - true - ) - } else { - this.setMessage( - this.$__( - "No items have been added to the train." - ) - ) - } - - this.$refs.table.redraw( - "/api/v1/preservation/waiting-list/items" - ) - this.show_modal_add_to_train = false - this.last_items = [] - }, - error => {} - ) - .then(() => this.loaded()) + let item_ids = this.last_items.map(i => i.item_id) + this.$router.push( + "/cgi-bin/koha/preservation/trains/" + + this.train_id_selected_for_add + + "/items/add/" + + item_ids.join(",") + ) }, addItemsToWaitingList: function (e) { e.preventDefault() diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js index 55325bdb57..f95d3705e1 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js @@ -3,6 +3,7 @@ import TrainsList from "../components/Preservation/TrainsList.vue"; import TrainsShow from "../components/Preservation/TrainsShow.vue"; import TrainsFormAdd from "../components/Preservation/TrainsFormAdd.vue"; import TrainsFormAddItem from "../components/Preservation/TrainsFormAddItem.vue"; +import TrainsFormAddItems from "../components/Preservation/TrainsFormAddItems.vue"; import WaitingList from "../components/Preservation/WaitingList.vue"; import Settings from "../components/Preservation/Settings.vue"; import SettingsProcessingsShow from "../components/Preservation/SettingsProcessingsShow.vue"; @@ -117,6 +118,17 @@ export const routes = [ ), }, }, + { + path: "add/:item_ids", + component: TrainsFormAddItems, + meta: { + breadcrumb: () => + build_breadcrumb( + breadcrumb_paths.trains, + "Add items to train" // $t("Add items to train") + ), + }, + }, { path: "edit/:train_item_id", component: TrainsFormAddItem, diff --git a/t/cypress/integration/Preservation/Trains.ts b/t/cypress/integration/Preservation/Trains.ts index b7a515c76b..858d523ebd 100644 --- a/t/cypress/integration/Preservation/Trains.ts +++ b/t/cypress/integration/Preservation/Trains.ts @@ -596,4 +596,64 @@ describe("Trains", () => { }); }); }); + + it("Add to waiting list then add to a train", () => { + let train = get_train(); + let processing = get_processings()[0]; + cy.intercept("GET", "/api/v1/preservation/trains*", [train]); + cy.intercept("GET", "/api/v1/preservation/trains/1", train); + cy.intercept("GET", "/api/v1/preservation/processings/1", processing); + cy.visit("/cgi-bin/koha/preservation/waiting-list"); + + cy.intercept("GET", "/api/v1/preservation/waiting-list/items*", { + statusCode: 200, + body: get_items(), + headers: { + "X-Base-Total-Count": "2", + "X-Total-Count": "2", + }, + }).as("get-items"); + cy.intercept("POST", "/api/v1/preservation/waiting-list/items", [ + { item_id: 1 }, + { item_id: 2 }, + ]); + cy.get("#waiting-list").contains("Add to waiting list").click(); + cy.get("#barcode_list").type("bc_1\nbc_2\nbc_3"); + cy.contains("Submit").click(); + cy.wait("@get-items"); + cy.get("main div[class='dialog message']").contains( + "2 new items added." + ); + cy.contains("Add last 2 items to a train").click(); + cy.get("#train_id .vs__search").type(train.name + "{enter}"); + cy.intercept("GET", "/api/v1/items*", { + statusCode: 200, + body: get_items().filter( + item => item.item_id == 1 || item.item_id == 2 + ), + headers: { + "X-Base-Total-Count": "2", + "X-Total-Count": "2", + }, + }); + cy.intercept( + "POST", + "/api/v1/preservation/trains/" + train.train_id + "/items/batch", + req => { + req.reply({ + statusCode: 201, + body: req.body, + }); + } + ); + cy.contains("Submit").click(); // Select train + train.items = get_train_items().filter( + train_item => train_item.item_id == 1 || train_item.item_id == 2 + ); + cy.intercept("GET", "/api/v1/preservation/trains/1", train); + cy.contains("Submit").click(); // Submit add items form + cy.get("main div[class='dialog message']").contains( + `2 items have been added to train ${train.train_id}.` + ); + }); }); diff --git a/t/cypress/integration/Preservation/WaitingList.ts b/t/cypress/integration/Preservation/WaitingList.ts index 4e4e1c2b5d..e5c956198f 100644 --- a/t/cypress/integration/Preservation/WaitingList.ts +++ b/t/cypress/integration/Preservation/WaitingList.ts @@ -103,52 +103,6 @@ describe("WaitingList", () => { ); }); - it("Add to waiting list then add to a train", () => { - let train = { - description: "yet another train", - name: "a train", - train_id: 1, - }; - cy.intercept("GET", "/api/v1/preservation/trains*", [train]); - cy.visit("/cgi-bin/koha/preservation/waiting-list"); - - cy.intercept("GET", "/api/v1/preservation/waiting-list/items*", { - statusCode: 200, - body: get_items(), - headers: { - "X-Base-Total-Count": "2", - "X-Total-Count": "2", - }, - }).as("get-items"); - cy.intercept("POST", "/api/v1/preservation/waiting-list/items", [ - { item_id: 1 }, - { item_id: 3 }, - ]); - cy.get("#waiting-list").contains("Add to waiting list").click(); - cy.get("#barcode_list").type("bc_1\nbc_2\nbc_3"); - cy.contains("Submit").click(); - cy.wait("@get-items"); - cy.get("main div[class='dialog message']").contains( - "2 new items added." - ); - cy.contains("Add last 2 items to a train").click(); - cy.get("#train_id .vs__search").type(train.name + "{enter}"); - cy.intercept( - "POST", - "/api/v1/preservation/trains/" + train.train_id + "/items/batch", - req => { - req.reply({ - statusCode: 201, - body: req.body, - }); - } - ); - cy.contains("Submit").click(); - cy.get("main div[class='dialog message']").contains( - `2 items have been added to train ${train.train_id}.` - ); - }); - it("Remove item from waiting list", () => { cy.intercept("GET", "/api/v1/preservation/waiting-list/items*", { statusCode: 200, -- 2.30.2