Bugzilla – Attachment 193728 Details for
Bug 14962
Temp Shelving Location / On Display Module
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 14962: On Display code for new Vue app
aa6995b.patch (text/plain), 47.72 KB, created by
Martin Renvoize (ashimema)
on 2026-02-24 13:35:39 UTC
(
hide
)
Description:
Bug 14962: On Display code for new Vue app
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2026-02-24 13:35:39 UTC
Size:
47.72 KB
patch
obsolete
>From aa6995b3d4db9ced746d136805bbe634c9b4769d Mon Sep 17 00:00:00 2001 >From: Jake Deery <jake.deery@openfifth.co.uk> >Date: Mon, 1 Dec 2025 14:41:21 +0000 >Subject: [PATCH] Bug 14962: On Display code for new Vue app > >This patch adds the On Display Vue app. Please make sure you rebuild your JavaScript prior to testing this patch. >Please see the test files commit for instructions on how to test this bundle of patches. > >Sponsored-by: ByWater Solutions >Signed-of-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >--- > .../Display/DisplaysBatchAddItems.vue | 177 ++++++ > .../Display/DisplaysBatchRemoveItems.vue | 153 +++++ > .../components/Display/DisplaysResource.vue | 531 ++++++++++++++++++ > .../prog/js/vue/components/Display/Home.vue | 9 + > .../prog/js/vue/components/Display/Main.vue | 115 ++++ > .../prog/js/vue/fetch/api-client.js | 8 + > .../prog/js/vue/fetch/http-client.js | 6 + > .../prog/js/vue/modules/display.ts | 72 +++ > .../prog/js/vue/routes/display.js | 82 +++ > .../prog/js/vue/stores/display.js | 27 + > 10 files changed, 1180 insertions(+) > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchAddItems.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchRemoveItems.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysResource.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Home.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Main.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/display.ts > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/routes/display.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/stores/display.js > >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchAddItems.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchAddItems.vue >new file mode 100644 >index 00000000000..a9b62f006fd >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchAddItems.vue >@@ -0,0 +1,177 @@ >+<template> >+ <h2>{{ $__("Batch add items from list") }}</h2> >+ <div class="page-section" id="list"> >+ <form @submit="batchAdd($event)"> >+ <fieldset class="rows" id="display_list"> >+ <h3>{{ $__("Sepcify items to add") }}:</h3> >+ <ol> >+ <li> >+ <label for="barcodes">{{ $__("Item barcodes") }}:</label> >+ <textarea >+ id="barcodes" >+ v-model="barcodes" >+ label="barcodes" >+ :cols="`25`" >+ :rows="`10`" >+ :required="!barcodes" >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ <div class="hint"> >+ {{ $__("List of item barcodes, one per line") }}<br /> >+ </div> >+ </li> >+ <li> >+ <label for="display_id" >+ >{{ $__("To the following display") }}:</label >+ > >+ <v-select >+ id="display_id" >+ v-model="display_id" >+ label="display_name" >+ :reduce="d => d.display_id" >+ :options="displays" >+ :clearable="false" >+ :required="!display_id" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required="!display_id" >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ <span class="required">{{ $__("Required") }}</span> >+ </li> >+ <li> >+ <label for="date_remove" >+ >{{ $__("To remove on this date") }}:</label >+ > >+ <FlatPickrWrapper >+ :id="`date_remove`" >+ :name="`date_remove`" >+ v-model="date_remove" >+ label="date_remove" >+ /> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <ButtonSubmit >+ :title="$__('Save')" >+ /> >+ <router-link >+ :to="{ >+ name: 'DisplaysList', >+ }" >+ role="button" >+ class="cancel" >+ >{{ $__("Cancel") }}</router-link >+ > >+ </fieldset> >+ </form> >+ </div> >+</template> >+ >+<script> >+import { ref, inject, onBeforeMount } from "vue"; >+import ButtonSubmit from "../ButtonSubmit.vue"; >+import FlatPickrWrapper from "@koha-vue/components/FlatPickrWrapper.vue"; >+import { storeToRefs } from "pinia"; >+import { APIClient } from "../../fetch/api-client.js"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ props: { >+ routeAction: String, >+ component: String, >+ embedded: { type: Boolean, default: false }, >+ componentPropData: Object, >+ embedEvent: Function, >+ }, >+ setup(props) { >+ const DisplayStore = inject("DisplayStore"); >+ const { config } = storeToRefs(DisplayStore); >+ const { setMessage, setWarning, setError } = inject("mainStore"); >+ >+ const displays = ref([]); >+ const display_id = ref(null); >+ const barcodes = ref(null); >+ const date_remove = ref(null); >+ >+ const batchAdd = event => { >+ event.preventDefault(); >+ >+ barcodes.value = barcodes.value >+ .split("\n") >+ .map(n => Number(n)) >+ .filter(n => { >+ if (n == '') >+ return false; >+ >+ return true; >+ }); >+ >+ const client = APIClient.display; >+ const importData = { >+ display_id: display_id.value, >+ barcodes: barcodes.value, >+ }; >+ if (date_remove.value != null) >+ importData.date_remove = date_remove.value; >+ >+ client.displayItems.batchAdd(importData).then( >+ success => { >+ if (success.job_id) >+ setMessage(`${$__('Batch job successfully queued.')} <a href="/cgi-bin/koha/admin/background_jobs.pl?op=view&id=${success.job_id}" target="_blank">${$__('Click here to view job progress')}</a>`, true); >+ >+ if (!success.job_id) >+ setWarning($__('Batch job failed to queue. Please check your list, and try again.'), true); >+ }, >+ error => { >+ setError($__('Internal Server Error. Please check the browser console for diagnostic information.'), true); >+ console.error(error); >+ }, >+ ); >+ clearForm(); >+ }; >+ const clearForm = () => { >+ display_id.value = null; >+ barcodes.value = null; >+ date_remove.value = null; >+ }; >+ >+ onBeforeMount(() => { >+ const client = APIClient.display; >+ client.displays.getAll().then( >+ result => { >+ displays.value = result; >+ }, >+ error => {} >+ ); >+ }); >+ return { >+ setMessage, >+ setWarning, >+ displays, >+ display_id, >+ barcodes, >+ date_remove, >+ batchAdd, >+ clearForm, >+ }; >+ }, >+ components: { >+ ButtonSubmit, >+ FlatPickrWrapper, >+ }, >+ name: "DisplaysBatchAddItems", >+}; >+</script> >+ >+<style scoped> >+label { >+ margin: 0px 10px 0px 0px; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchRemoveItems.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchRemoveItems.vue >new file mode 100644 >index 00000000000..4656eecfa9c >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchRemoveItems.vue >@@ -0,0 +1,153 @@ >+<template> >+ <h2>{{ $__("Batch remove items from list") }}</h2> >+ <div class="page-section" id="list"> >+ <form @submit="batchRemove($event)"> >+ <fieldset class="rows" id="display_list"> >+ <h3>{{ $__("Specify items to remove") }}:</h3> >+ <ol> >+ <li> >+ <label for="barcodes">{{ $__("Item barcodes") }}:</label> >+ <textarea >+ id="barcodes" >+ v-model="barcodes" >+ label="barcodes" >+ :cols="`25`" >+ :rows="`10`" >+ :required="!barcodes" >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ <div class="hint"> >+ {{ $__("List of item barcodes, one per line") }}<br /> >+ </div> >+ </li> >+ <li> >+ <label for="display_id" >+ >{{ $__("From the following display") }}:</label >+ > >+ <v-select >+ id="display_id" >+ v-model="display_id" >+ label="display_name" >+ :reduce="d => d.display_id" >+ :options="displays" >+ :clearable="false" >+ :required="!display_id" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required="!display_id" >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ <span class="required">{{ $__("Required") }}</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <ButtonSubmit >+ :title="$__('Save')" >+ /> >+ <router-link >+ :to="{ >+ name: 'DisplaysList', >+ }" >+ role="button" >+ class="cancel" >+ >{{ $__("Cancel") }}</router-link >+ > >+ </fieldset> >+ </form> >+ </div> >+</template> >+ >+<script> >+import { ref, inject, useTemplateRef, onBeforeMount } from "vue"; >+import ButtonSubmit from "../ButtonSubmit.vue"; >+import { storeToRefs } from "pinia"; >+import { APIClient } from "../../fetch/api-client.js"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ props: { >+ routeAction: String, >+ embedded: { type: Boolean, default: false }, >+ embedEvent: Function, >+ }, >+ setup(props) { >+ const DisplayStore = inject("DisplayStore"); >+ const { config } = storeToRefs(DisplayStore); >+ const { setMessage, setWarning, setError } = inject("mainStore"); >+ >+ const displays = ref([]); >+ const display_id = ref(null); >+ const barcodes = ref(null); >+ >+ const batchRemove = event => { >+ event.preventDefault(); >+ >+ barcodes.value = barcodes.value >+ .split("\n") >+ .map(n => Number(n)) >+ .filter(n => { >+ if (n == '') >+ return false; >+ >+ return true; >+ }); >+ >+ const client = APIClient.display; >+ const importData = { >+ display_id: display_id.value, >+ barcodes: barcodes.value, >+ }; >+ >+ client.displayItems.batchDelete(importData).then( >+ success => { >+ setMessage(`${$__('Batch job successfully queued.')} <a href="/cgi-bin/koha/admin/background_jobs.pl" target="_blank">${$__('Click here to view job progress')}</a>`, true); >+ }, >+ error => { >+ setError($__('Internal Server Error. Please check the browser console for diagnostic information.'), true); >+ console.error(error); >+ }, >+ ); >+ clearForm(); >+ }; >+ const clearForm = () => { >+ display_id.value = null; >+ barcodes.value = null; >+ }; >+ >+ onBeforeMount(() => { >+ const client = APIClient.display; >+ client.displays.getAll().then( >+ result => { >+ displays.value = result; >+ }, >+ error => {} >+ ); >+ }); >+ return { >+ setMessage, >+ setWarning, >+ displays, >+ display_id, >+ barcodes, >+ batchRemove, >+ clearForm, >+ }; >+ }, >+ components: { >+ ButtonSubmit, >+ }, >+ name: "DisplaysBatchRemoveItems", >+}; >+</script> >+ >+<style scoped> >+label { >+ margin: 0px 10px 0px 0px; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysResource.vue >new file mode 100644 >index 00000000000..7f2545a775c >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysResource.vue >@@ -0,0 +1,531 @@ >+<template> >+ <BaseResource >+ :routeAction="routeAction" >+ :instancedResource="this" >+ ></BaseResource> >+</template> >+<script> >+import { inject, onBeforeMount } from "vue"; >+import BaseResource from "../BaseResource.vue"; >+import { useBaseResource } from "../../composables/base-resource.js"; >+import { storeToRefs } from "pinia"; >+import { APIClient } from "../../fetch/api-client.js"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ props: { >+ routeAction: String, >+ embedded: { type: Boolean, default: false }, >+ embedEvent: Function, >+ }, >+ setup(props) { >+ const DisplayStore = inject("DisplayStore"); >+ const { displayReturnOverMapping, config } = storeToRefs(DisplayStore); >+ const { setError } = inject("mainStore"); >+ >+ const filters = []; >+ >+ const additionalToolbarButtons = resource => { >+ return { >+ list: [ >+ { >+ to: { name: "DisplaysBatchAddItems" }, >+ icon: "plus", >+ title: $__("Batch add items from list"), >+ }, >+ { >+ to: { name: "DisplaysBatchRemoveItems" }, >+ icon: "minus", >+ title: $__("Batch remove items from list"), >+ }, >+ ], >+ }; >+ }; >+ >+ const baseResource = useBaseResource({ >+ resourceName: "displays", >+ nameAttr: "display_name", >+ idAttr: "display_id", >+ components: { >+ show: "DisplaysShow", >+ list: "DisplaysList", >+ add: "DisplaysFormAdd", >+ edit: "DisplaysFormAddEdit", >+ }, >+ apiClient: APIClient.display.displays, >+ i18n: { >+ deleteConfirmationMessage: $__( >+ "Are you sure you want to remove this display?" >+ ), >+ deleteSuccessMessage: $__("Display %s deleted"), >+ displayName: $__("Display"), >+ editLabel: $__("Edit display #%s"), >+ emptyListMessage: $__("There are no displays defined"), >+ newLabel: $__("New display"), >+ }, >+ table: { >+ addFilters: true, >+ resourceTableUrl: >+ APIClient.display.httpClientDisplays._baseURL + "", >+ filters, >+ }, >+ embedded: props.embedded, >+ config, >+ props, >+ resourceAttrs: [ >+ { >+ name: "display_id", >+ label: $__("ID"), >+ type: "text", >+ hideIn: ["Form", "Show"], >+ }, >+ { >+ name: "display_name", >+ label: $__("Name"), >+ type: "text", >+ required: true, >+ }, >+ { >+ name: "display_branch", >+ label: $__("Home library"), >+ type: "relationshipSelect", >+ relationshipAPIClient: >+ APIClient.library.libraries, >+ relationshipOptionLabelAttr: "name", >+ relationshipRequiredKey: "library_id", >+ tableColumnDefinition: { >+ title: $__("Home library"), >+ data: "display_branch", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ if (row.home_library === null) >+ return (escape_str( >+ `` >+ )); >+ else >+ return (escape_str( >+ `${row["home_library"]["name"]}` >+ )); >+ }, >+ }, >+ showElement: { >+ type: "text", >+ value: "home_library.name" >+ }, >+ }, >+ { >+ name: "display_holding_branch", >+ label: $__("Holding library"), >+ type: "relationshipSelect", >+ relationshipAPIClient: >+ APIClient.library.libraries, >+ relationshipOptionLabelAttr: "name", >+ relationshipRequiredKey: "library_id", >+ tableColumnDefinition: { >+ title: $__("Holding library"), >+ data: "display_holding_branch", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ if (row.holding_library === null) >+ return (escape_str( >+ `` >+ )); >+ else >+ return (escape_str( >+ `${row["holding_library"]["name"]}` >+ )); >+ }, >+ }, >+ showElement: { >+ type: "text", >+ value: "holding_library.name" >+ }, >+ }, >+ { >+ name: "display_location", >+ label: $__("Shelving location"), >+ type: "select", >+ avCat: "av_loc", >+ }, >+ { >+ name: "display_code", >+ label: $__("Collection code"), >+ type: "select", >+ avCat: "av_ccode", >+ }, >+ { >+ name: "display_itype", >+ label: $__("Item type"), >+ type: "relationshipSelect", >+ relationshipAPIClient: >+ APIClient.item_type.item_types, >+ relationshipOptionLabelAttr: "description", >+ relationshipRequiredKey: "item_type_id", >+ tableColumnDefinition: { >+ title: $__("Item type"), >+ data: "display_itype", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ if (row.item_type === null) >+ return (escape_str( >+ `` >+ )); >+ else >+ return (escape_str( >+ `${row["item_type"]["description"]}` >+ )); >+ }, >+ }, >+ showElement: { >+ type: "text", >+ value: "item_type.description" >+ }, >+ }, >+ { >+ name: "display_return_over", >+ label: $__("Return behaviour"), >+ hint: $__("Remove items from display on checkin"), >+ type: "select", >+ selectLabel: "value", >+ requiredKey: "variable", >+ options: displayReturnOverMapping.value, >+ defaultValue: null, >+ required: true, >+ tableColumnDefinition: { >+ title: $__("Return behaviour"), >+ data: "display_return_over", >+ searchable: false, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ let this_value = ''; >+ >+ DisplayStore.displayReturnOverMapping.forEach(mapping => { >+ if(mapping.variable == data) this_value = mapping.value; >+ }); >+ >+ return (escape_str( >+ `${this_value}` >+ )); >+ }, >+ }, >+ }, >+ { >+ name: "start_date", >+ label: $__("Start of display"), >+ hint: $__("When the display becomes effective"), >+ type: "date", >+ }, >+ { >+ name: "end_date", >+ label: $__("End of display"), >+ hint: $__("When the display's effectiveness ends"), >+ type: "date", >+ }, >+ { >+ name: "display_days", >+ label: $__("Duration of display"), >+ hint: $__("Days in which items will remain on display"), >+ type: "number", >+ hideIn: ["List"], >+ }, >+ { >+ name: "staff_note", >+ label: $__("Staff note"), >+ hint: $__("Notes only visible on staff client"), >+ type: "textarea", >+ hideIn: ["List"], >+ }, >+ { >+ name: "public_note", >+ label: $__("Public note"), >+ hint: $__("Notes visible on both staff client and OPAC"), >+ type: "textarea", >+ hideIn: ["List"], >+ }, >+ { >+ name: "enabled", >+ label: $__("Enabled"), >+ type: "boolean", >+ required: true, >+ }, >+ { >+ name: "display_items", >+ type: "relationshipWidget", >+ showElement: { >+ type: "table", >+ columnData: "display_items", >+ hidden: display => !!display.display_items?.length, >+ columns: [ >+ { >+ name: $__("Record number"), >+ value: "biblionumber", >+ link: { >+ href: "/cgi-bin/koha/catalogue/detail.pl", >+ params: { >+ biblionumber: "biblionumber", >+ }, >+ }, >+ }, >+ { >+ name: $__("Internal item number"), >+ value: "itemnumber", >+ link: { >+ href: "/cgi-bin/koha/catalogue/moredetail.pl", >+ params: { >+ itemnumber: "itemnumber", >+ }, >+ }, >+ }, >+ { >+ name: $__("Item barcode"), >+ value: "barcode", >+ link: { >+ href: "/cgi-bin/koha/catalogue/moredetail.pl", >+ params: { >+ itemnumber: "itemnumber", >+ }, >+ }, >+ }, >+ { >+ name: $__("Date added"), >+ value: "date_added", >+ format: $date, >+ }, >+ { >+ name: $__("Date to remove"), >+ value: "date_remove", >+ format: $date, >+ }, >+ ], >+ }, >+ group: $__("Display items"), >+ componentProps: { >+ resourceRelationships: { >+ resourceProperty: "display_items", >+ }, >+ relationshipI18n: { >+ nameUpperCase: __("Display item"), >+ removeThisMessage: __( >+ "Remove this display item" >+ ), >+ addNewMessage: __("Add new display item"), >+ noneCreatedYetMessage: __( >+ "There are no display items created yet" >+ ), >+ }, >+ newRelationshipDefaultAttrs: { >+ type: "object", >+ value: { >+ biblionumber: null, >+ itemnumber: null, >+ barcode: null, >+ date_added: null, >+ date_remove: null, >+ }, >+ }, >+ }, >+ relationshipFields: [ >+ { >+ name: "barcode", >+ type: "text", >+ label: $__("Item barcode"), >+ required: true, >+ indexRequired: true, >+ }, >+ { >+ name: "date_added", >+ type: "date", >+ label: $__("Date added"), >+ required: false, >+ indexRequired: true, >+ }, >+ { >+ name: "date_remove", >+ type: "date", >+ label: $__("Date to remove"), >+ required: false, >+ indexRequired: true, >+ }, >+ ], >+ hideIn: ["List"], >+ }, >+ ], >+ additionalToolbarButtons, >+ moduleStore: "DisplayStore", >+ props: props, >+ }); >+ >+ const tableOptions = { >+ url: "/api/v1/displays/", >+ options: { >+ embed: "home_library,holding_library,item_type,+strings", >+ }, >+ add_filters: true, >+ actions: { >+ 0: ["show"], >+ 1: ["show"], >+ "-1": ["edit", "delete"] >+ }, >+ }; >+ >+ const getItemFromId = (async id => { >+ const itemsApiClient = APIClient.item.items; >+ let item = undefined; >+ >+ await itemsApiClient.get(id) >+ .then(data => { >+ item = data; >+ }) >+ .catch(error => { >+ console.error(error); >+ }); >+ >+ return item; >+ }); >+ >+ const getItemFromExternalId = (async external_id => { >+ const itemsApiClient = APIClient.item.items; >+ let item = undefined; >+ >+ await itemsApiClient.getByExternalId(external_id) >+ .then(data => { >+ if (data.length == 1) >+ item = data[0]; >+ }) >+ .catch(error => { >+ console.error(error); >+ }); >+ >+ return item; >+ }); >+ >+ const checkForm = (async display => { >+ let errors = []; >+ >+ let display_items = display.display_items; >+ // Do not use di.display_item.name here! Its name is not the one linked with di.display_item_id >+ // At this point di.display_item is meaningless, form/template only modified di.display_item_id >+ const display_item_ids = display_items.map(di => di.display_item_id); >+ const duplicate_display_item_ids = display_item_ids.filter( >+ (id, i) => display_item_ids.indexOf(id) !== i >+ ); >+ >+ if (duplicate_display_item_ids.length) { >+ errors.push($__("A display item is used several times")); >+ } >+ >+ for await (const display_item of display_items) { >+ const item = await getItemFromExternalId(display_item.barcode); >+ >+ if (item == undefined || item.item_id === undefined || item.external_id !== display_item.barcode) >+ errors.push($__("The barcode entered does not match an item")); >+ } >+ >+ baseResource.setWarning(errors.join("<br>")); >+ return !errors.length; >+ }); >+ const onFormSave = (async (e, displayToSave) => { >+ e.preventDefault(); >+ >+ const display = JSON.parse(JSON.stringify(displayToSave)); >+ const displayId = display.display_id; >+ const epoch = new Date(); >+ >+ if (!await checkForm(display)) { >+ return false; >+ } >+ >+ delete display.display_id; >+ delete display.item_type; >+ delete display.home_library; >+ delete display.holding_library; >+ delete display._strings; >+ >+ display.display_items = display.display_items.map( >+ ({ display_item_id, ...keepAttrs }) => >+ keepAttrs >+ ); >+ >+ let display_items = display.display_items; >+ delete display.display_items; >+ display.display_items = []; >+ >+ for await (const display_item of display_items) { >+ const item = await getItemFromExternalId(display_item.barcode); >+ >+ delete display_item.barcode; >+ >+ display_item.biblionumber = item.biblio_id; >+ display_item.itemnumber = item.item_id; >+ >+ await display.display_items.push(display_item); >+ } >+ >+ if (display.start_date == null) display.start_date = epoch.toISOString().substr(0, 10); >+ if (display.end_date == null && display.display_days != undefined) { >+ let calculated_date = epoch; >+ calculated_date.setDate(epoch.getDate() + Number(display.display_days)); >+ >+ display.end_date = calculated_date.toISOString().substr(0, 10); >+ } >+ if (display.display_days == "") display.display_days = null; >+ if (display.public_note == "") display.public_note = null; >+ if (display.staff_note == "") display.staff_note = null; >+ >+ if (displayId) { >+ baseResource.apiClient >+ .update(display, displayId) >+ .then( >+ success => { >+ baseResource.setMessage($__("Display updated")); >+ baseResource.router.push({ name: "DisplaysList" }); >+ }, >+ error => {} >+ ); >+ } else { >+ baseResource.apiClient.create(display).then( >+ success => { >+ baseResource.setMessage($__("Display created")); >+ baseResource.router.push({ name: "DisplaysList" }); >+ }, >+ error => {} >+ ); >+ } >+ }); >+ const afterResourceFetch = ((componentData, resource, caller) => { >+ if(caller === "show" || caller === "form") { >+ resource.display_items.forEach((display_item, idx) => { >+ getItemFromId(display_item.itemnumber) >+ .then(item => { >+ componentData.resource.value.display_items[idx] = { >+ barcode: item.external_id, >+ ...display_item, >+ }; >+ }) >+ .catch(error => { >+ console.error(error); >+ }); >+ }); >+ } >+ }); >+ >+ onBeforeMount(() => {}); >+ >+ return { >+ ...baseResource, >+ tableOptions, >+ checkForm, >+ onFormSave, >+ afterResourceFetch, >+ }; >+ }, >+ emits: ["select-resource"], >+ name: "DisplaysResource", >+ components: { >+ BaseResource, >+ }, >+}; >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Home.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Home.vue >new file mode 100644 >index 00000000000..2968a34c108 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Home.vue >@@ -0,0 +1,9 @@ >+<template> >+ <div id="home"></div> >+</template> >+ >+<script> >+export default { >+ components: {}, >+}; >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Main.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Main.vue >new file mode 100644 >index 00000000000..e41ae4b170a >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/Main.vue >@@ -0,0 +1,115 @@ >+<template> >+ <div v-if="initialized && config.settings.enabled == 1"> >+ <div id="sub-header"> >+ <Breadcrumbs /> >+ <Help /> >+ </div> >+ <div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-md-10 order-md-2 order-sm-1"> >+ <main> >+ <Dialog /> >+ <router-view /> >+ </main> >+ </div> >+ >+ <div class="col-md-2 order-sm-2 order-md-1"> >+ <LeftMenu :title="$__('Displays')"></LeftMenu> >+ </div> >+ </div> >+ </div> >+ </div> >+ <div class="main container-fluid" v-else> >+ <Dialog /> >+ </div> >+</template> >+ >+<script> >+import { inject, onBeforeMount, ref } from "vue"; >+import Breadcrumbs from "../Breadcrumbs.vue"; >+import Help from "../Help.vue"; >+import LeftMenu from "../LeftMenu.vue"; >+import Dialog from "../Dialog.vue"; >+import { APIClient } from "../../fetch/api-client.js"; >+import "vue-select/dist/vue-select.css"; >+import { storeToRefs } from "pinia"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ setup() { >+ const mainStore = inject("mainStore"); >+ >+ const { loading, loaded, setError } = mainStore; >+ >+ const DisplayStore = inject("DisplayStore"); >+ >+ const { config, authorisedValues } = storeToRefs(DisplayStore); >+ const { loadAuthorisedValues } = DisplayStore; >+ >+ const initialized = ref(false); >+ >+ onBeforeMount(() => { >+ loading(); >+ >+ const client = APIClient.display; >+ client.config.get().then(result => { >+ config.value = result; >+ if (config.value.settings.enabled != 1) { >+ loaded(); >+ return setError( >+ $__( >+ 'The displays module is disabled, turn on <a href="/cgi-bin/koha/admin/preferences.pl?tab=&op=search&searchfield=UseDisplayModule">UseDisplayModule</a> to use it' >+ ), >+ false >+ ); >+ } >+ >+ DisplayStore.displayReturnOverMapping.push({ >+ "variable": "yes - any library", >+ "value": $__('Yes, any library'), >+ }); >+ DisplayStore.displayReturnOverMapping.push({ >+ "variable": "yes - except at home library", >+ "value": $__('Yes, except at home library'), >+ }); >+ DisplayStore.displayReturnOverMapping.push({ >+ "variable": "no", >+ "value": $__('No'), >+ }); >+ >+ loadAuthorisedValues( >+ authorisedValues.value, >+ DisplayStore >+ ).then(() => { >+ loaded(); >+ initialized.value = true; >+ }); >+ >+ }); >+ }); >+ >+ return { >+ loading, >+ loaded, >+ config, >+ setError, >+ DisplayStore, >+ initialized, >+ }; >+ }, >+ components: { >+ Breadcrumbs, >+ Dialog, >+ Help, >+ LeftMenu, >+ }, >+}; >+</script> >+ >+<style> >+form .v-select { >+ display: inline-block; >+ background-color: white; >+ width: 30%; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js >index 2906a9c296b..52d258df719 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js >@@ -6,7 +6,11 @@ import AcquisitionAPIClient from "@fetch/acquisition-api-client"; > import AdditionalFieldsAPIClient from "@fetch/additional-fields-api-client"; > import AVAPIClient from "@fetch/authorised-values-api-client"; > import CashAPIClient from "@fetch/cash-api-client"; >+import BiblioAPIClient from '@fetch/biblio-api-client.js'; >+import DisplayAPIClient from "@fetch/display-api-client"; > import ItemAPIClient from "@fetch/item-api-client"; >+import ItemTypeAPIClient from '@fetch/item-type-api-client.js'; >+import LibraryAPIClient from "@fetch/library-api-client"; > import RecordSourcesAPIClient from "@fetch/record-sources-api-client"; > import SysprefAPIClient from "@fetch/system-preferences-api-client"; > import SIP2APIClient from "@fetch/sip2-api-client"; >@@ -19,7 +23,11 @@ export const APIClient = { > additional_fields: new AdditionalFieldsAPIClient(HttpClient), > authorised_values: new AVAPIClient(HttpClient), > cash: new CashAPIClient(HttpClient), >+ biblio: new BiblioAPIClient(HttpClient), >+ display: new DisplayAPIClient(HttpClient), > item: new ItemAPIClient(HttpClient), >+ item_type: new ItemTypeAPIClient(HttpClient), >+ library: new LibraryAPIClient(HttpClient), > sysprefs: new SysprefAPIClient(HttpClient), > sip2: new SIP2APIClient(HttpClient), > preservation: new PreservationAPIClient(HttpClient), >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js >index f31856bf106..64551056e93 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js >@@ -150,6 +150,11 @@ class HttpClient { > } > > delete(params = {}) { >+ const body = params.body >+ ? typeof params.body === "string" >+ ? params.body >+ : JSON.stringify(params.body) >+ : undefined; > let csrf_token = { "CSRF-TOKEN": this.csrf_token }; > let headers = { ...csrf_token, ...params.headers }; > return this._fetchJSON( >@@ -158,6 +163,7 @@ class HttpClient { > { > parseResponse: false, > ...params.options, >+ body, > method: "DELETE", > }, > params.return_response ?? true, >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/display.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/display.ts >new file mode 100644 >index 00000000000..c8ff1734554 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/display.ts >@@ -0,0 +1,72 @@ >+import { createApp } from "vue"; >+import { createWebHistory, createRouter } from "vue-router"; >+import { createPinia } from "pinia"; >+ >+import { library } from "@fortawesome/fontawesome-svg-core"; >+import { >+ faPlus, >+ faMinus, >+ faPencil, >+ faTrash, >+ faSpinner, >+ faClose, >+ faPaperPlane, >+ faInbox, >+} from "@fortawesome/free-solid-svg-icons"; >+import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; >+import vSelect from "vue-select"; >+ >+library.add( >+ faPlus, >+ faMinus, >+ faPencil, >+ faTrash, >+ faSpinner, >+ faClose, >+ faPaperPlane, >+ faInbox >+); >+ >+import App from "../components/Display/Main.vue"; >+ >+import { routes as routesDef } from "../routes/display"; >+ >+import { useMainStore } from "../stores/main"; >+import { useDisplayStore } from "../stores/display"; >+import { useNavigationStore } from "../stores/navigation"; >+import i18n from "@koha-vue/i18n"; >+ >+const pinia = createPinia(); >+ >+const mainStore = useMainStore(pinia); >+const navigationStore = useNavigationStore(pinia); >+const routes = navigationStore.setRoutes(routesDef); >+ >+const router = createRouter({ >+ history: createWebHistory(), >+ linkActiveClass: "current", >+ routes, >+}); >+ >+const app = createApp(App); >+ >+const rootComponent = app >+ .use(i18n) >+ .use(pinia) >+ .use(router) >+ .component("font-awesome-icon", FontAwesomeIcon) >+ .component("v-select", vSelect); >+ >+app.config.unwrapInjectedRef = true; >+app.provide("mainStore", mainStore); >+app.provide("navigationStore", navigationStore); >+const DisplayStore = useDisplayStore(pinia); >+app.provide("DisplayStore", DisplayStore); >+ >+app.mount("#display"); >+ >+const { removeMessages } = mainStore; >+router.beforeEach((to, from) => { >+ navigationStore.$patch({ current: to.matched, params: to.params || {} }); >+ removeMessages(); // This will actually flag the messages as displayed already >+}); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/display.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/display.js >new file mode 100644 >index 00000000000..2e5b882db9f >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/display.js >@@ -0,0 +1,82 @@ >+import { markRaw } from "vue"; >+ >+import Home from "../components/Display/Home.vue"; >+import DisplaysBatchAddItems from "../components/Display/DisplaysBatchAddItems.vue"; >+import DisplaysBatchRemoveItems from "../components/Display/DisplaysBatchRemoveItems.vue"; >+ >+import ResourceWrapper from "../components/ResourceWrapper.vue"; >+ >+import { $__ } from "@koha-vue/i18n"; >+ >+export const routes = [ >+ { >+ path: "/cgi-bin/koha/display/display-home.pl", >+ is_default: true, >+ is_base: true, >+ title: $__("Displays"), >+ children: [ >+ { >+ path: "", >+ name: "Home", >+ component: markRaw(Home), >+ redirect: "/cgi-bin/koha/display/displays", >+ is_navigation_item: false, >+ }, >+ { >+ path: "/cgi-bin/koha/display/displays", >+ title: $__("Displays"), >+ icon: "fa-solid fa-image-portrait", >+ is_end_node: true, >+ resource: "Display/DisplaysResource.vue", >+ children: [ >+ { >+ path: "", >+ name: "DisplaysList", >+ component: markRaw(ResourceWrapper), >+ }, >+ { >+ path: ":display_id", >+ name: "DisplaysShow", >+ component: markRaw(ResourceWrapper), >+ title: "{display_name}", >+ }, >+ { >+ path: "add", >+ name: "DisplaysFormAdd", >+ component: markRaw(ResourceWrapper), >+ title: $__("Add display"), >+ }, >+ { >+ path: "edit/:display_id", >+ name: "DisplaysFormAddEdit", >+ component: markRaw(ResourceWrapper), >+ title: "{display_name}", >+ breadcrumbFormat: ({ match, params, query }) => { >+ match.name = "DisplaysShow"; >+ return match; >+ }, >+ additionalBreadcrumbs: [ >+ { title: $__("Modify display"), disabled: true }, >+ ], >+ }, >+ { >+ path: "batch-add", >+ name: "DisplaysBatchAddItems", >+ component: markRaw( >+ DisplaysBatchAddItems >+ ), >+ title: $__("Batch add items from list"), >+ }, >+ { >+ path: "batch-remove", >+ name: "DisplaysBatchRemoveItems", >+ component: markRaw( >+ DisplaysBatchRemoveItems >+ ), >+ title: $__("Batch remove items from list"), >+ }, >+ ], >+ }, >+ ], >+ }, >+]; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/display.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/display.js >new file mode 100644 >index 00000000000..894fa1fbfd4 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/display.js >@@ -0,0 +1,27 @@ >+import { defineStore } from "pinia"; >+import { reactive, toRefs } from "vue"; >+import { withAuthorisedValueActions } from "../composables/authorisedValues"; >+ >+export const useDisplayStore = defineStore("display", () => { >+ const store = reactive({ >+ displayReturnOverMapping: [], >+ config: { >+ settings: { >+ enabled: 0, >+ }, >+ }, >+ authorisedValues: { >+ av_loc: "LOC", >+ av_ccode: "CCODE", >+ }, >+ }); >+ >+ const sharedActions = { >+ ...withAuthorisedValueActions(store), >+ }; >+ >+ return { >+ ...toRefs(store), >+ ...sharedActions, >+ }; >+}); >-- >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 14962
:
190216
|
190217
|
190218
|
190219
|
190220
|
190221
|
190222
|
190223
|
190224
|
190225
|
190226
|
190227
|
190228
|
190229
|
190230
|
190231
|
190232
|
190571
|
190572
|
193719
|
193720
|
193721
|
193722
|
193723
|
193724
|
193725
|
193726
|
193727
|
193728
|
193729
|
193730
|
193731
|
193732
|
193733
|
193734
|
193735
|
193736
|
193737
|
193738
|
193739
|
193740
|
193741
|
193742
|
193743
|
193744
|
193745
|
193746
|
193747
|
193748
|
193749
|
193750
|
193751
|
193752
|
193753
|
193754
|
193755
|
194285
|
194286
|
194287
|
194288
|
194289
|
194290
|
194291
|
194292
|
194293
|
194294
|
194295
|
194296
|
194297
|
194298
|
194299
|
194300
|
194301
|
194302
|
194303
|
194304
|
194305
|
194306
|
194307
|
194308
|
194309
|
194310
|
194311
|
194312
|
194313
|
194314
|
194315
|
194316
|
194317
|
194318
|
194320
|
194321
|
194322