Bugzilla – Attachment 190225 Details for
Bug 14962
Temp Shelving Location
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
Bug-14962-On-Display-code-for-new-Vue-app.patch (text/plain), 47.14 KB, created by
Jake Deery
on 2025-12-05 14:51:38 UTC
(
hide
)
Description:
Bug 14962: On Display code for new Vue app
Filename:
MIME Type:
Creator:
Jake Deery
Created:
2025-12-05 14:51:38 UTC
Size:
47.14 KB
patch
obsolete
>From 772dcf52ef1373bb3ac3eca4a885496488ddb527 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. >--- > .../Display/DisplaysBatchAddItems.vue | 191 +++++++ > .../Display/DisplaysBatchRemoveItems.vue | 169 ++++++ > .../components/Display/DisplaysResource.vue | 501 ++++++++++++++++++ > .../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..1558b2aa8cf >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchAddItems.vue >@@ -0,0 +1,191 @@ >+<template> >+ <h2>{{ $__("Batch add items from file") }}</h2> >+ <div class="page-section" id="files"> >+ <form @submit="batchAdd($event)" class="file_upload"> >+ <fieldset class="rows" id="display_list"> >+ <h3>{{ $__("Select file for upload") }}:</h3> >+ <ol> >+ <li> >+ <label for="import_file">{{ $__("File") }}:</label> >+ <input >+ type="file" >+ @change="selectFile($event)" >+ :id="`import_file`" >+ :name="`import_file`" >+ required="required" >+ ref="fileLoader" >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ </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 /> >+ <a @click="clearForm()" role="button" class="cancel">{{ >+ $__("Clear form") >+ }}</a> >+ </fieldset> >+ </form> >+ </div> >+</template> >+ >+<script> >+import { ref, inject, useTemplateRef, 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, >+ embedded: { type: Boolean, default: false }, >+ embedEvent: Function, >+ }, >+ setup(props) { >+ const DisplayStore = inject("DisplayStore"); >+ const { config } = storeToRefs(DisplayStore); >+ const { setMessage, setWarning, setError } = inject("mainStore"); >+ const fileLoader = useTemplateRef("fileLoader"); >+ >+ const displays = ref([]); >+ const display_id = ref(null); >+ const file = ref({ >+ filename: null, >+ file_content: null, >+ }); >+ const item_ids = ref(null); >+ const date_remove = ref(null); >+ >+ const selectFile = event => { >+ let files = event.target.files; >+ if (!files) >+ return; >+ let newFile = files[0]; >+ const reader = new FileReader(); >+ reader.onload = event => loadFile(newFile.name, event.target.result); >+ reader.readAsText(newFile); >+ }; >+ const loadFile = (filename, content) => { >+ file.value.filename = filename; >+ file.value.file_content = content; >+ file.value.file_as_array = content.split("\n") || undefined; >+ }; >+ const batchAdd = event => { >+ event.preventDefault(); >+ >+ item_ids.value = file.value.file_as_array >+ .map(n => Number(n)) >+ .filter(n => { >+ if (n == '') >+ return false; >+ >+ return true; >+ }); >+ >+ const client = APIClient.display; >+ const importData = { >+ display_id: display_id.value, >+ item_ids: item_ids.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; >+ file.value = { >+ filename: null, >+ file_content: null, >+ file_as_array: null, >+ }; >+ fileLoader.files = null; >+ fileLoader.value = null; >+ date_remove.value = null; >+ }; >+ >+ onBeforeMount(() => { >+ const client = APIClient.display; >+ client.displays.getAll().then( >+ result => { >+ displays.value = result; >+ }, >+ error => {} >+ ); >+ }); >+ return { >+ setMessage, >+ setWarning, >+ fileLoader, >+ displays, >+ display_id, >+ file, >+ selectFile, >+ 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..90288bfb8f1 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysBatchRemoveItems.vue >@@ -0,0 +1,169 @@ >+<template> >+ <h2>{{ $__("Batch remove items from file") }}</h2> >+ <div class="page-section" id="files"> >+ <form @submit="batchRemove($event)" class="file_upload"> >+ <fieldset class="rows" id="display_list"> >+ <h3>{{ $__("Select file for upload") }}:</h3> >+ <ol> >+ <li> >+ <label for="import_file">{{ $__("File") }}:</label> >+ <input >+ type="file" >+ @change="selectFile($event)" >+ :id="`import_file`" >+ :name="`import_file`" >+ required="required" >+ ref="fileLoader" >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ </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> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <ButtonSubmit /> >+ <a @click="clearForm()" role="button" class="cancel">{{ >+ $__("Clear form") >+ }}</a> >+ </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 fileLoader = useTemplateRef("fileLoader"); >+ >+ const displays = ref([]); >+ const display_id = ref(null); >+ const file = ref({ >+ filename: null, >+ file_content: null, >+ }); >+ const item_ids = ref(null); >+ >+ const selectFile = event => { >+ let files = event.target.files; >+ if (!files) >+ return; >+ let newFile = files[0]; >+ const reader = new FileReader(); >+ reader.onload = event => loadFile(newFile.name, event.target.result); >+ reader.readAsText(newFile); >+ }; >+ const loadFile = (filename, content) => { >+ file.value.filename = filename; >+ file.value.file_content = content; >+ file.value.file_as_array = content.split("\n") || undefined; >+ }; >+ const batchRemove = event => { >+ event.preventDefault(); >+ >+ item_ids.value = file.value.file_as_array >+ .map(n => Number(n)) >+ .filter(n => { >+ if (n == '') >+ return false; >+ >+ return true; >+ }); >+ >+ const client = APIClient.display; >+ const importData = { >+ display_id: display_id.value, >+ item_ids: item_ids.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 all jobs')}</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; >+ file.value = { >+ filename: null, >+ file_content: null, >+ file_as_array: null, >+ }; >+ fileLoader.files = null; >+ fileLoader.value = null; >+ }; >+ >+ onBeforeMount(() => { >+ const client = APIClient.display; >+ client.displays.getAll().then( >+ result => { >+ displays.value = result; >+ }, >+ error => {} >+ ); >+ }); >+ return { >+ setMessage, >+ setWarning, >+ fileLoader, >+ displays, >+ display_id, >+ file, >+ selectFile, >+ 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..5ff2ba08db2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Display/DisplaysResource.vue >@@ -0,0 +1,501 @@ >+<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 file"), >+ }, >+ { >+ to: { name: "DisplaysBatchRemoveItems" }, >+ icon: "minus", >+ title: $__("Batch remove items from file"), >+ }, >+ ], >+ }; >+ }; >+ >+ 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.library === null) >+ return (escape_str( >+ `` >+ )); >+ else >+ return (escape_str( >+ `${row["library"]["name"]}` >+ )); >+ }, >+ }, >+ showElement: { >+ type: "text", >+ value: "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.library === null) >+ return (escape_str( >+ `` >+ )); >+ else >+ return (escape_str( >+ `${row["library"]["name"]}` >+ )); >+ }, >+ }, >+ showElement: { >+ type: "text", >+ value: "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"), >+ 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"), >+ type: "date", >+ }, >+ { >+ name: "end_date", >+ label: $__("End of display"), >+ type: "date", >+ }, >+ { >+ name: "display_days", >+ label: $__("Duration of display"), >+ type: "number", >+ hideIn: ["List"], >+ }, >+ { >+ name: "staff_note", >+ label: $__("Staff note"), >+ type: "textarea", >+ hideIn: ["List"], >+ }, >+ { >+ name: "public_note", >+ label: $__("Public note"), >+ 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", >+ }, >+ { >+ name: $__("Internal item number"), >+ value: "itemnumber", >+ }, >+ { >+ name: $__("Item barcode"), >+ value: "barcode", >+ }, >+ { >+ 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", >+ }, >+ relationshipStrings: { >+ nameLowerCase: $__("display item"), >+ nameUpperCase: $__("Display item"), >+ namePlural: $__("display items"), >+ }, >+ newRelationshipDefaultAttrs: { >+ type: "object", >+ value: { >+ biblionumber: null, >+ itemnumber: null, >+ barcode: null, >+ date_added: null, >+ date_remove: null, >+ }, >+ }, >+ }, >+ relationshipFields: [ >+ { >+ name: "barcode", >+ type: "number", >+ 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: "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.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.log(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..b83cd47b751 >--- /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 file"), >+ }, >+ { >+ path: "batch-remove", >+ name: "DisplaysBatchRemoveItems", >+ component: markRaw( >+ DisplaysBatchRemoveItems >+ ), >+ title: $__("Batch remove items from file"), >+ }, >+ ], >+ }, >+ ], >+ }, >+]; >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.50.1 (Apple Git-155)
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