From 8b9446d803bcf922620bc24c95625b949746e69e Mon Sep 17 00:00:00 2001 From: Tomas Cohen Arazi <tomascohen@theke.io> Date: Wed, 24 Jan 2024 16:18:13 -0300 Subject: [PATCH] Bug 35919: Add record sources admin page This patch introduces a Vue.js based record sources managing page. To test it: 1. Apply this patch 2. Build the Vue.js stuff: $ ktd --shell k$ yarn js:build k$ restart_all 3. On the staff interface, go to Administration > Record sources 4. Play with the interface and the offered actions => SUCCESS: Things go well 5. Sign off :-D Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> Signed-off-by: Matt Blenkinsop <matt.blenkinsop@ptfs-europe.com> Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> --- admin/record_sources.pl | 37 +++++ debian/templates/apache-shared-intranet.conf | 2 + .../prog/en/modules/admin/admin-home.tt | 4 + .../prog/en/modules/admin/record_sources.tt | 34 +++++ .../Admin/RecordSources/FormAdd.vue | 121 +++++++++++++++ .../components/Admin/RecordSources/List.vue | 140 ++++++++++++++++++ .../components/Admin/RecordSources/Main.vue | 34 +++++ .../prog/js/vue/fetch/api-client.js | 2 + .../js/vue/fetch/record-sources-api-client.js | 51 +++++++ .../js/vue/modules/admin/record_sources.ts | 54 +++++++ .../js/vue/routes/admin/record_sources.js | 38 +++++ webpack.config.js | 1 + 12 files changed, 518 insertions(+) create mode 100755 admin/record_sources.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/record_sources.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/FormAdd.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/List.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/Main.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/fetch/record-sources-api-client.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/routes/admin/record_sources.js diff --git a/admin/record_sources.pl b/admin/record_sources.pl new file mode 100755 index 00000000000..9524f32dd65 --- /dev/null +++ b/admin/record_sources.pl @@ -0,0 +1,37 @@ +#!/usr/bin/perl + +# Copyright 2023 Theke Solutions +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see <http://www.gnu.org/licenses>. + +use Modern::Perl; + +use CGI qw ( -utf8 ); +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); + +my $query = CGI->new; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "admin/record_sources.tt", + query => $query, + type => "intranet", + flagsrequired => { parameters => 'manage_record_sources' }, + } +); + +output_html_with_http_headers $query, $cookie, $template->output; diff --git a/debian/templates/apache-shared-intranet.conf b/debian/templates/apache-shared-intranet.conf index b4fe0ea2263..835b0812b6b 100644 --- a/debian/templates/apache-shared-intranet.conf +++ b/debian/templates/apache-shared-intranet.conf @@ -13,6 +13,7 @@ ScriptAlias /search "/usr/share/koha/intranet/cgi-bin/catalogue/search.pl" # Protect dev package install RewriteEngine on + RewriteRule ^/cgi-bin/koha/(C4|debian|etc|installer/data|install_misc|Koha|misc|selenium|t|test|tmp|xt)/|\.PL$ /notfound [PT] RewriteRule ^/bib/([^\/]*)/?$ /cgi-bin/koha/catalogue/detail.pl?biblionumber=$1 [PT] @@ -23,6 +24,7 @@ RewriteRule ^(.*)_[0-9]{2}\.[0-9]{7}\.(js|css)$ $1.$2 [L] RewriteRule ^/cgi-bin/koha/erm/.*$ /cgi-bin/koha/erm/erm.pl [PT] RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$ RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT] +RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT] Alias "/api" "/usr/share/koha/api" <Directory "/usr/share/koha/api"> diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt index 1320214b653..8f920678ba6 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -210,6 +210,10 @@ <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></dt> <dd>Manage indexes, facets, and their mappings to MARC fields and subfields</dd> [% END %] + [% IF ( CAN_user_parameters_manage_record_sources ) %] + <dt><a href="/cgi-bin/koha/admin/record_sources">Record sources</a></dt> + <dd>Define record sources to import from</dd> + [% END %] </dl> [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/record_sources.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/record_sources.tt new file mode 100644 index 00000000000..533a171ec2c --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/record_sources.tt @@ -0,0 +1,34 @@ +[% USE raw %] +[% USE To %] +[% USE Asset %] +[% USE KohaDates %] +[% USE TablesSettings %] +[% USE AuthorisedValues %] +[% SET footerjs = 1 %] +[% PROCESS 'i18n.inc' %] +[% INCLUDE 'doc-head-open.inc' %] +<title> + Record sources › Koha +</title> +[% INCLUDE 'doc-head-close.inc' %] +</head> + +<body id="record_sources" class="record_sources"> +[% WRAPPER 'header.inc' %] + [% INCLUDE 'prefs-admin-search.inc' %] +[% END %] + +<div id="record-source"> <!-- this is closed in intranet-bottom.inc --> + +[% MACRO jsinclude BLOCK %] + [% INCLUDE 'calendar.inc' %] <!-- FIXME: this shouldn't be needed --> + [% INCLUDE 'datatables.inc' %] + [% INCLUDE 'columns_settings.inc' %] + [% INCLUDE 'js-patron-format.inc' %] + [% INCLUDE 'js-date-format.inc' %] + + [% Asset.js("js/vue/dist/admin/record_sources.js") | $raw %] + +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/FormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/FormAdd.vue new file mode 100644 index 00000000000..2b7994224cb --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/FormAdd.vue @@ -0,0 +1,121 @@ +<template> + <div v-if="!initialized">{{ $__("Loading") }}</div> + <div v-else id="record_source_edit"> + <h1 v-if="record_source.record_source_id"> + {{ $__("Edit '%s'").format(record_source.name) }} + </h1> + <h1 v-else>{{ $__("Add record source") }}</h1> + <form @submit="onSubmit($event)"> + <fieldset class="rows"> + <ol> + <li> + <label class="required" for="name"> + {{ $__("Name") }}: + </label> + <input + id="name" + v-model="record_source.name" + required + /> + <span class="required">{{ $__("Required") }}</span> + </li> + <li> + <label for="can_be_edited"> + {{ $__("Can be edited") }}: + </label> + <input + id="can_be_edited" + type="checkbox" + v-model="record_source.can_be_edited" + /> + </li> + </ol> + </fieldset> + <fieldset class="action"> + <input type="submit" :value="$__('Submit')" /> + <router-link + :to="{ name: 'RecordSourcesList' }" + role="button" + class="cancel" + >{{ $__("Cancel") }}</router-link + > + </fieldset> + </form> + </div> +</template> + +<script> +import { inject } from "vue" +import { setMessage, setError, setWarning } from "../../../messages" +import { APIClient } from "../../../fetch/api-client.js" + +export default { + setup() { + const { setMessage } = inject("mainStore") + return { + setMessage, + } + }, + data() { + return { + record_source: { + record_source_id: null, + name: "", + can_be_edited: false, + }, + initialized: false, + } + }, + beforeRouteEnter(to, from, next) { + next(vm => { + if (to.params.record_source_id) { + vm.getRecordSource(to.params.record_source_id) + } else { + vm.initialized = true + } + }) + }, + methods: { + async getRecordSource(record_source_id) { + const client = APIClient.record_sources + client.record_sources.get(record_source_id).then( + record_source => { + this.record_source = record_source + this.record_source_id = record_source_id + this.initialized = true + }, + error => {} + ) + }, + onSubmit(e) { + e.preventDefault() + const client = APIClient.record_sources + let response + // RO attribute + delete this.record_source.record_source_id + if (this.record_source_id) { + // update + response = client.record_sources + .update(this.record_source, this.record_source_id) + .then( + success => { + setMessage(this.$__("Record source updated!")) + this.$router.push({ name: "RecordSourcesList" }) + }, + error => {} + ) + } else { + response = client.record_sources + .create(this.record_source) + .then( + success => { + setMessage(this.$__("Record source created!")) + this.$router.push({ name: "RecordSourcesList" }) + }, + error => {} + ) + } + }, + }, +} +</script> diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/List.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/List.vue new file mode 100644 index 00000000000..f8776f9b35d --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/List.vue @@ -0,0 +1,140 @@ +<template> + <div v-if="!initialized">{{ $__("Loading") }}</div> + <div v-else id="record_sources_list"> + <Toolbar> + <ToolbarButton + :to="{ name: 'RecordSourcesFormAdd' }" + icon="plus" + :title="$__('New record source')" + /> + </Toolbar> + <h1>{{ title }}</h1> + <div v-if="record_sources_count > 0" class="page-section"> + <KohaTable + ref="table" + v-bind="tableOptions" + @edit="doEdit" + @delete="doDelete" + ></KohaTable> + </div> + <div v-else class="dialog message"> + {{ $__("There are no record sources defined") }} + </div> + </div> +</template> + +<script> +import Toolbar from "../../Toolbar.vue" +import ToolbarButton from "../../ToolbarButton.vue" +import { inject } from "vue" +import { APIClient } from "../../../fetch/api-client.js" +import KohaTable from "../../KohaTable.vue" + +export default { + data() { + return { + title: this.$__("Record sources"), + tableOptions: { + columns: [ + { + title: this.$__("ID"), + data: "record_source_id", + searchable: true, + }, + { + title: this.$__("Name"), + data: "name", + searchable: true, + }, + { + title: __("Can be edited"), + data: "can_be_edited", + searchable: true, + orderable: true, + render: function (data, type, row, meta) { + return escape_str( + row.can_be_edited ? __("Yes") : __("No") + ) + }, + }, + ], + actions: { + "-1": ["edit", "delete"], + }, + url: "/api/v1/record_sources", + }, + initialized: false, + record_sources_count: 0, + } + }, + setup() { + const { setWarning, setMessage, setError, setConfirmationDialog } = + inject("mainStore") + return { + setWarning, + setMessage, + setError, + setConfirmationDialog, + } + }, + beforeRouteEnter(to, from, next) { + next(vm => { + vm.getRecordSourcesCount().then(() => (vm.initialized = true)) + }) + }, + methods: { + async getRecordSourcesCount() { + const client = APIClient.record_sources + await client.record_sources.count().then( + count => { + this.record_sources_count = count + }, + error => {} + ) + }, + newRecordSource() { + this.$router.push({ name: "RecordSourcesFormAdd" }) + }, + doEdit: function ({ record_source_id }, dt, event) { + this.$router.push({ + name: "RecordSourcesFormAddEdit", + params: { record_source_id }, + }) + }, + doDelete: function (record_source, dt, event) { + this.setConfirmationDialog( + { + title: this.$__( + "Are you sure you want to remove this record source?" + ), + message: record_source.name, + accept_label: this.$__("Yes, delete"), + cancel_label: this.$__("No, do not delete"), + }, + () => { + const client = APIClient.record_sources + client.record_sources + .delete(record_source.record_source_id) + .then( + success => { + this.setMessage( + this.$__("Record source %s deleted").format( + record_source.name + ), + true + ) + dt.draw() + }, + error => {} + ) + } + ) + }, + }, + components: { + KohaTable, + Toolbar, + ToolbarButton, + }, +} +</script> diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/Main.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/Main.vue new file mode 100644 index 00000000000..057c78a4ae1 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/RecordSources/Main.vue @@ -0,0 +1,34 @@ +<template> + <div> + <div id="sub-header"> + <Breadcrumbs></Breadcrumbs> + <Help /> + </div> + <div class="main container-fluid"> + <div class="row"> + <div class="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> + <main> + <Dialog></Dialog> + <router-view /> + </main> + </div> + </div> + </div> + </div> +</template> + +<script> +import Breadcrumbs from "../../Breadcrumbs.vue" +import Help from "../../Help.vue" +import Dialog from "../../Dialog.vue" + +export default { + components: { + Breadcrumbs, + Dialog, + Help, + }, +} +</script> + +<style></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 294693c238e..4bfd3c8dd0c 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 @@ -3,6 +3,7 @@ import PatronAPIClient from "./patron-api-client"; import AcquisitionAPIClient from "./acquisition-api-client"; import AVAPIClient from "./authorised-values-api-client"; import ItemAPIClient from "./item-api-client"; +import RecordSourcesAPIClient from "./record-sources-api-client"; import SysprefAPIClient from "./system-preferences-api-client"; import PreservationAPIClient from "./preservation-api-client"; @@ -14,4 +15,5 @@ export const APIClient = { item: new ItemAPIClient(), sysprefs: new SysprefAPIClient(), preservation: new PreservationAPIClient(), + record_sources: new RecordSourcesAPIClient(), }; diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/record-sources-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/record-sources-api-client.js new file mode 100644 index 00000000000..b12882d1dca --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/record-sources-api-client.js @@ -0,0 +1,51 @@ +import HttpClient from "./http-client"; + +export class RecordSourcesAPIClient extends HttpClient { + constructor() { + super({ + baseURL: "/api/v1/record_sources", + }); + } + + get record_sources() { + return { + create: record_source => + this.post({ + endpoint: "", + body: record_source, + }), + delete: id => + this.delete({ + endpoint: "/" + id, + }), + update: (record_source, id) => + this.put({ + endpoint: "/" + id, + body: record_source, + }), + get: id => + this.get({ + endpoint: "/" + id, + }), + getAll: (query, params) => + this.getAll({ + endpoint: "/", + query, + params, + headers: {}, + }), + count: (query = {}) => + this.count({ + endpoint: + "?" + + new URLSearchParams({ + _page: 1, + _per_page: 1, + ...(query && { q: JSON.stringify(query) }), + }), + }), + }; + } +} + +export default RecordSourcesAPIClient; diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts new file mode 100644 index 00000000000..80f43f4ea2f --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts @@ -0,0 +1,54 @@ +import { createApp } from "vue"; +import { createPinia } from "pinia"; +import { createWebHistory, createRouter } from "vue-router"; + +import { library } from "@fortawesome/fontawesome-svg-core"; +import { + faPlus, + faMinus, + faPencil, + faTrash, + faSpinner, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; +import vSelect from "vue-select"; +import { useNavigationStore } from "../../stores/navigation"; +import { useMainStore } from "../../stores/main"; +import routesDef from "../../routes/admin/record_sources"; + +library.add(faPlus, faMinus, faPencil, faTrash, faSpinner); + +const pinia = createPinia(); +const navigationStore = useNavigationStore(pinia); +const mainStore = useMainStore(pinia); +const { removeMessages } = mainStore; +const { setRoutes } = navigationStore; +const routes = setRoutes(routesDef); + +const router = createRouter({ + history: createWebHistory(), + linkExactActiveClass: "current", + routes, +}); + +import App from "../../components/Admin/RecordSources/Main.vue"; +import i18n from "../../i18n"; + +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); +app.mount("#record-source"); + +router.beforeEach(to => { + 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/admin/record_sources.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/admin/record_sources.js new file mode 100644 index 00000000000..a85fd4b5f41 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/admin/record_sources.js @@ -0,0 +1,38 @@ +import { markRaw } from "vue"; +import RecordSourcesFormAdd from "../../components/Admin/RecordSources/FormAdd.vue"; +import RecordSourcesList from "../../components/Admin/RecordSources/List.vue"; +import { $__ } from "../../i18n"; + +export default { + title: $__("Administration"), + path: "", + href: "/cgi-bin/koha/admin/admin-home.pl", + is_base: true, + is_default: true, + children: [ + { + title: $__("Record sources"), + path: "/cgi-bin/koha/admin/record_sources", + is_end_node: true, + children: [ + { + path: "", + name: "RecordSourcesList", + component: markRaw(RecordSourcesList), + }, + { + component: markRaw(RecordSourcesFormAdd), + name: "RecordSourcesFormAdd", + path: "add", + title: $__("Add record source"), + }, + { + component: markRaw(RecordSourcesFormAdd), + name: "RecordSourcesFormAddEdit", + path: "edit/:record_source_id", + title: $__("Edit record source"), + }, + ], + }, + ], +}; diff --git a/webpack.config.js b/webpack.config.js index c22256785a4..ae87653c244 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,6 +7,7 @@ module.exports = { entry: { erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts", preservation: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts", + "admin/record_sources": "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts", }, output: { filename: "[name].js", -- 2.34.1