Bugzilla – Attachment 149777 Details for
Bug 32607
Add record sources CRUD
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 32607: Add CRUD for import sources
Bug-32607-Add-CRUD-for-import-sources.patch (text/plain), 64.35 KB, created by
Agustín Moyano
on 2023-04-17 20:02:58 UTC
(
hide
)
Description:
Bug 32607: Add CRUD for import sources
Filename:
MIME Type:
Creator:
Agustín Moyano
Created:
2023-04-17 20:02:58 UTC
Size:
64.35 KB
patch
obsolete
>From af6095735867ad746aca33c6cad2cd4f0bc49beb Mon Sep 17 00:00:00 2001 >From: Agustin Moyano <agustinmoyano@theke.io> >Date: Thu, 2 Feb 2023 21:07:55 -0300 >Subject: [PATCH] Bug 32607: Add CRUD for import sources > >This patch adds vue components to create, read, update and delete import sources > >To test: >1. apply patches >2. updatedatabase >3. yarn build_js >4. go to Administration -> Import sources >5. play around with the ui and create, update, remove and search import sources >6. Sign off ;) >--- > Koha/ImportSource.pm | 56 +++ > Koha/ImportSources.pm | 51 +++ > Koha/REST/V1/ImportSources.pm | 141 ++++++++ > Koha/Schema/Result/ImportSource.pm | 23 ++ > admin/import-sources.pl | 38 +++ > api/v1/swagger/definitions/import_source.yaml | 13 + > api/v1/swagger/paths/import_sources.yaml | 247 ++++++++++++++ > api/v1/swagger/swagger.yaml | 15 + > .../prog/en/modules/admin/admin-home.tt | 2 + > .../prog/en/modules/admin/import-sources.tt | 37 ++ > .../prog/js/vue/components/Form.vue | 144 ++++++++ > .../prog/js/vue/components/Page.vue | 53 +++ > .../vue/components/import-sources/ISEdit.vue | 80 +++++ > .../vue/components/import-sources/ISList.vue | 100 ++++++ > .../vue/components/import-sources/ISMain.vue | 28 ++ > .../prog/js/vue/fetch/import-sources.js | 40 +++ > .../prog/js/vue/fetch/patron-api-client.js | 4 + > .../intranet-tmpl/prog/js/vue/i18n/index.js | 2 +- > .../prog/js/vue/import-source/router.js | 12 + > .../prog/js/vue/inputs/autocomplete.js | 199 +++++++++++ > .../intranet-tmpl/prog/js/vue/inputs/index.js | 5 + > .../prog/js/vue/models/import-sources.js | 85 +++++ > .../prog/js/vue/modules/import-sources.ts | 76 +++++ > .../prog/js/vue/routes/import-sources.js | 31 ++ > .../intranet-tmpl/prog/js/vue/stores/model.js | 51 +++ > package.json | 2 + > t/db_dependent/Koha/ImportSources.t | 44 +++ > t/db_dependent/api/v1/import_sources.t | 318 ++++++++++++++++++ > webpack.config.js | 1 + > 29 files changed, 1897 insertions(+), 1 deletion(-) > create mode 100644 Koha/ImportSource.pm > create mode 100644 Koha/ImportSources.pm > create mode 100644 Koha/REST/V1/ImportSources.pm > create mode 100755 admin/import-sources.pl > create mode 100644 api/v1/swagger/definitions/import_source.yaml > create mode 100644 api/v1/swagger/paths/import_sources.yaml > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js > create mode 100755 t/db_dependent/Koha/ImportSources.t > create mode 100755 t/db_dependent/api/v1/import_sources.t > >diff --git a/Koha/ImportSource.pm b/Koha/ImportSource.pm >new file mode 100644 >index 0000000000..b23fc95422 >--- /dev/null >+++ b/Koha/ImportSource.pm >@@ -0,0 +1,56 @@ >+package Koha::ImportSource; >+ >+# This file is part of Koha. >+# >+# Copyright 2020 Koha Development Team >+# >+# 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 base qw(Koha::Object); >+ >+=head1 NAME >+ >+Koha::ImportSource - Koha ImportSource Object class >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=head3 patron >+ >+my $patron = $import_source->patron >+ >+Return the patron for this import source >+ >+=cut >+ >+sub patron { >+ my ($self) = @_; >+ >+ my $patron_rs = $self->_result->patron; >+ return Koha::Patron->_new_from_dbic($patron_rs); >+} >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'ImportSource'; >+} >+ >+1; >\ No newline at end of file >diff --git a/Koha/ImportSources.pm b/Koha/ImportSources.pm >new file mode 100644 >index 0000000000..1cdca37b1b >--- /dev/null >+++ b/Koha/ImportSources.pm >@@ -0,0 +1,51 @@ >+package Koha::ImportSources; >+ >+# This file is part of Koha. >+# >+# Copyright 2020 Koha Development Team >+# >+# 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 base qw(Koha::Objects); >+ >+use Koha::ImportSource; >+ >+=head1 NAME >+ >+Koha::ImportSources - Koha ImportSources Object class >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'ImportSource'; >+} >+ >+=head3 object_class >+ >+=cut >+ >+sub object_class { >+ return 'Koha::ImportSource'; >+} >+ >+1; >\ No newline at end of file >diff --git a/Koha/REST/V1/ImportSources.pm b/Koha/REST/V1/ImportSources.pm >new file mode 100644 >index 0000000000..b77e2cadca >--- /dev/null >+++ b/Koha/REST/V1/ImportSources.pm >@@ -0,0 +1,141 @@ >+package Koha::REST::V1::ImportSources; >+ >+# 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 Mojo::Base 'Mojolicious::Controller'; >+ >+use Koha::ImportSources; >+ >+use Try::Tiny qw( catch try ); >+ >+=head1 API >+ >+=head2 Methods >+ >+=head3 list >+ >+=cut >+ >+sub list { >+ my $c = shift->openapi->valid_input or return; >+ >+ return try { >+ my $import_sources_set = Koha::ImportSources->new; >+ my $import_sources = $c->objects->search( $import_sources_set ); >+ return $c->render( status => 200, openapi => $import_sources ); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+=head3 get >+ >+=cut >+ >+sub get { >+ my $c = shift->openapi->valid_input or return; >+ >+ return try { >+ my $import_sources_set = Koha::ImportSources->new; >+ my $import_source = $c->objects->find( $import_sources_set, $c->validation->param('import_source_id') ); >+ unless ($import_source) { >+ return $c->render( status => 404, >+ openapi => { error => "Import source not found" } ); >+ } >+ >+ return $c->render( status => 200, openapi => $import_source ); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+=head3 add >+ >+=cut >+ >+sub add { >+ my $c = shift->openapi->valid_input or return; >+ >+ return try { >+ my $import_source = Koha::ImportSource->new_from_api( $c->validation->param('body') ); >+ $import_source->store; >+ $c->res->headers->location( $c->req->url->to_string . '/' . $import_source->import_source_id ); >+ return $c->render( >+ status => 201, >+ openapi => $import_source->to_api >+ ); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+=head3 update >+ >+=cut >+ >+sub update { >+ my $c = shift->openapi->valid_input or return; >+ >+ my $import_source = Koha::ImportSources->find( $c->validation->param('import_source_id') ); >+ >+ if ( not defined $import_source ) { >+ return $c->render( status => 404, >+ openapi => { error => "Object not found" } ); >+ } >+ >+ return try { >+ $import_source->set_from_api( $c->validation->param('body') ); >+ $import_source->store(); >+ return $c->render( status => 200, openapi => $import_source->to_api ); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+=head3 delete >+ >+=cut >+ >+sub delete { >+ my $c = shift->openapi->valid_input or return; >+ >+ my $import_source = Koha::ImportSources->find( $c->validation->param('import_source_id') ); >+ if ( not defined $import_source ) { >+ return $c->render( status => 404, >+ openapi => { error => "Object not found" } ); >+ } >+ >+ return try { >+ $import_source->delete; >+ return $c->render( >+ status => 204, >+ openapi => q{} >+ ); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+1; >diff --git a/Koha/Schema/Result/ImportSource.pm b/Koha/Schema/Result/ImportSource.pm >index a902cb0892..fa44d0a526 100644 >--- a/Koha/Schema/Result/ImportSource.pm >+++ b/Koha/Schema/Result/ImportSource.pm >@@ -86,4 +86,27 @@ __PACKAGE__->belongs_to( > > > # You can replace this text with custom code or comments, and it will be preserved on regeneration >+ >+=head1 CUSTOM CODE >+ >+=head2 koha_object_class >+ >+name of the corresponding Koha::Object >+ >+=cut >+ >+sub koha_object_class { >+ 'Koha::ImportSource'; >+} >+ >+=head2 koha_objects_class >+ >+name of the corresponding Koha::Objects >+ >+=cut >+ >+sub koha_objects_class { >+ 'Koha::ImportSources'; >+} >+ > 1; >diff --git a/admin/import-sources.pl b/admin/import-sources.pl >new file mode 100755 >index 0000000000..c23c53270a >--- /dev/null >+++ b/admin/import-sources.pl >@@ -0,0 +1,38 @@ >+#!/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 ); >+use Koha::Plugins; >+ >+my $query = CGI->new; >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => "admin/import-sources.tt", >+ query => $query, >+ type => "intranet", >+ flagsrequired => { parameters => 'manage_import_sources' }, >+ } >+); >+ >+output_html_with_http_headers $query, $cookie, $template->output; >diff --git a/api/v1/swagger/definitions/import_source.yaml b/api/v1/swagger/definitions/import_source.yaml >new file mode 100644 >index 0000000000..b7d3555d1d >--- /dev/null >+++ b/api/v1/swagger/definitions/import_source.yaml >@@ -0,0 +1,13 @@ >+--- >+type: object >+properties: >+ import_source_id: >+ type: integer >+ description: internally assigned import source identifier >+ readOnly: true >+ name: >+ description: import source name >+ type: string >+ patron_id: >+ description: linked patron identifier >+ type: integer >diff --git a/api/v1/swagger/paths/import_sources.yaml b/api/v1/swagger/paths/import_sources.yaml >new file mode 100644 >index 0000000000..63c8b50214 >--- /dev/null >+++ b/api/v1/swagger/paths/import_sources.yaml >@@ -0,0 +1,247 @@ >+/import_sources: >+ get: >+ x-mojo-to: ImportSources#list >+ operationId: listImportSources >+ summary: List import sources >+ tags: >+ - import_sources >+ parameters: >+ - $ref: "../swagger.yaml#/parameters/match" >+ - $ref: "../swagger.yaml#/parameters/order_by" >+ - $ref: "../swagger.yaml#/parameters/page" >+ - $ref: "../swagger.yaml#/parameters/per_page" >+ - $ref: "../swagger.yaml#/parameters/q_param" >+ - $ref: "../swagger.yaml#/parameters/q_body" >+ - $ref: "../swagger.yaml#/parameters/q_header" >+ - $ref: "../swagger.yaml#/parameters/request_id_header" >+ - name: x-koha-embed >+ in: header >+ required: false >+ description: Embed list sent as a request header >+ type: array >+ items: >+ type: string >+ enum: >+ - patron >+ collectionFormat: csv >+ consumes: >+ - application/json >+ produces: >+ - application/json >+ responses: >+ "200": >+ description: A list of import sources >+ "400": >+ description: Missing or wrong parameters >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Not allowed >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ parameters: manage_import_sources >+ post: >+ x-mojo-to: ImportSources#add >+ operationId: addImportSources >+ summary: Add an import source >+ tags: >+ - import_sources >+ parameters: >+ - name: body >+ in: body >+ description: A JSON object containing informations about the new import source >+ required: true >+ schema: >+ $ref: "../swagger.yaml#/definitions/import_source" >+ consumes: >+ - application/json >+ produces: >+ - application/json >+ responses: >+ "201": >+ description: An import source >+ "400": >+ description: Missing or wrong parameters >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Not allowed >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ parameters: manage_import_sources >+"/import_sources/{import_source_id}": >+ get: >+ x-mojo-to: ImportSources#get >+ operationId: getImportSources >+ summary: Get an import source >+ tags: >+ - import_sources >+ parameters: >+ - $ref: "../swagger.yaml#/parameters/import_source_id_pp" >+ consumes: >+ - application/json >+ produces: >+ - application/json >+ responses: >+ "200": >+ description: An import source >+ "400": >+ description: Missing or wrong parameters >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Not allowed >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: Not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ parameters: manage_import_sources >+ put: >+ x-mojo-to: ImportSources#update >+ operationId: updateImportSources >+ summary: Update an import source >+ tags: >+ - import_sources >+ parameters: >+ - $ref: "../swagger.yaml#/parameters/import_source_id_pp" >+ - name: body >+ in: body >+ description: A JSON object containing informations about the new import source >+ required: true >+ schema: >+ $ref: "../swagger.yaml#/definitions/import_source" >+ consumes: >+ - application/json >+ produces: >+ - application/json >+ responses: >+ "200": >+ description: An import source >+ "400": >+ description: Missing or wrong parameters >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Not allowed >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: Not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ parameters: manage_import_sources >+ delete: >+ x-mojo-to: ImportSources#delete >+ operationId: deleteImportSources >+ summary: Delete an import source >+ tags: >+ - import_sources >+ parameters: >+ - $ref: "../swagger.yaml#/parameters/import_source_id_pp" >+ consumes: >+ - application/json >+ produces: >+ - application/json >+ responses: >+ "204": >+ description: Deleted >+ "400": >+ description: Missing or wrong parameters >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Not allowed >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: Not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ parameters: manage_import_sources >diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml >index 7d5e04f62d..f18c0c81a5 100644 >--- a/api/v1/swagger/swagger.yaml >+++ b/api/v1/swagger/swagger.yaml >@@ -58,6 +58,8 @@ definitions: > $ref: ./definitions/import_batch_profiles.yaml > import_record_match: > $ref: ./definitions/import_record_match.yaml >+ import_source: >+ $ref: ./definitions/import_source.yaml > invoice: > $ref: ./definitions/invoice.yaml > item: >@@ -251,6 +253,10 @@ paths: > $ref: ./paths/import_batch_profiles.yaml#/~1import_batch_profiles > "/import_batch_profiles/{import_batch_profile_id}": > $ref: "./paths/import_batch_profiles.yaml#/~1import_batch_profiles~1{import_batch_profile_id}" >+ /import_sources: >+ $ref: ./paths/import_sources.yaml#/~1import_sources >+ "/import_sources/{import_source_id}": >+ $ref: ./paths/import_sources.yaml#/~1import_sources~1{import_source_id} > /items: > $ref: ./paths/items.yaml#/~1items > "/items/{item_id}": >@@ -493,6 +499,12 @@ parameters: > name: import_record_id > required: true > type: integer >+ import_source_id_pp: >+ description: Internal import source identifier >+ in: path >+ name: import_source_id >+ required: true >+ type: integer > item_id_pp: > description: Internal item identifier > in: path >@@ -792,6 +804,9 @@ tags: > - description: "Manage item groups\n" > name: item_groups > x-displayName: Item groups >+ - description: "Manage import sources\n" >+ name: import_sources >+ x-displayName: Import source > - description: "Manage items\n" > name: items > x-displayName: Items >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 1ba35ac919..e05c516709 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 >@@ -206,6 +206,8 @@ > <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 %] >+ <dt><a href="/cgi-bin/koha/admin/import-sources.pl">Import sources</a></dt> >+ <dd>Define sources to import from</dd> > </dl> > [% END %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt >new file mode 100644 >index 0000000000..63fd41f541 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt >@@ -0,0 +1,37 @@ >+[% USE raw %] >+[% USE To %] >+[% USE Asset %] >+[% USE KohaDates %] >+[% USE TablesSettings %] >+[% USE AuthorisedValues %] >+[% SET footerjs = 1 %] >+[% PROCESS 'i18n.inc' %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title> >+ Import sources › Koha >+</title> >+[% INCLUDE 'doc-head-close.inc' %] >+</head> >+ >+<body id="erm_agreements" class="erm"> >+[% WRAPPER 'header.inc' %] >+ [% INCLUDE 'erm-search.inc' %] >+[% END %] >+ >+<div id="import-source"> <!-- this is closed in intranet-bottom.inc --> >+ >+[% MACRO jsinclude BLOCK %] >+ [% INCLUDE 'calendar.inc' %] >+ [% INCLUDE 'datatables.inc' %] >+ [% INCLUDE 'columns_settings.inc' %] >+ [% INCLUDE 'js-patron-format.inc' %] >+ [% INCLUDE 'js-date-format.inc' %] >+ >+ <script> >+ const RESTOAuth2ClientCredentials = [% IF Koha.Preference('RESTOAuth2ClientCredentials') %]true[% ELSE %]false[% END %]; >+ </script> >+ >+ [% Asset.js("js/vue/dist/import-source.js") | $raw %] >+ >+[% END %] >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue >new file mode 100644 >index 0000000000..fcdfa5a773 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue >@@ -0,0 +1,144 @@ >+<template> >+ <FormKit >+ v-if="!loading" >+ ref="form" >+ @submit="doSubmit" >+ type="form" >+ :actions="false" >+ :value="data" >+ > >+ <FormKitSchema :schema="formSchema" :data="schemaData"></FormKitSchema> >+ </FormKit> >+ <div v-else>Loading...</div> >+</template> >+<script> >+import { FormKitSchema } from "@formkit/vue" >+import { $__ } from "../i18n" >+ >+export default { >+ name: "Form", >+ data() { >+ return { >+ schemaData: { >+ doCancel: () => { >+ this.$emit("cancel") >+ }, >+ }, >+ formSchema: [ >+ { >+ $el: "fieldset", >+ attrs: { >+ class: "rows", >+ }, >+ children: this.schema, >+ }, >+ { >+ $el: "fieldset", >+ attrs: { >+ class: "action", >+ }, >+ children: [ >+ { >+ $el: "input", >+ attrs: { >+ type: "submit", >+ class: "btn btn-primary", >+ }, >+ ignore: false, >+ children: this.submitMessage, >+ }, >+ { >+ $el: "a", >+ attrs: { >+ class: "cancel", >+ onClick: "$doCancel", >+ }, >+ children: this.cancelMessage, >+ }, >+ ], >+ }, >+ ], >+ } >+ }, >+ methods: { >+ async doSubmit(event) { >+ this.$emit("submit", event) >+ }, >+ }, >+ components: { >+ FormKitSchema, >+ }, >+ emits: ["submit", "cancel"], >+ props: { >+ schema: Object, >+ loading: { >+ type: Boolean, >+ default: false, >+ }, >+ data: { >+ type: Object, >+ default: {}, >+ }, >+ submitMessage: { >+ type: String, >+ default: $__("Submit"), >+ }, >+ cancelMessage: { >+ type: String, >+ default: $__("Cancel"), >+ }, >+ }, >+} >+</script> >+ >+<style> >+.formkit-value { >+ align-items: center; >+ background-color: #f5f5f5; >+ border-radius: 0.25em; >+ box-sizing: border-box; >+ color: black; >+ display: flex; >+ justify-content: space-between; >+ padding: 0.55em 0.5em; >+ text-decoration: none; >+ width: 100%; >+} >+ >+a.formkit-value:hover { >+ text-decoration: none; >+} >+ >+.formkit-value::after { >+ content: "\00D7"; >+ margin-left: 0.5em; >+ font-size: 1.1em; >+} >+ >+.formkit-dropdown { >+ position: absolute; >+ top: 100%; >+ left: 0; >+ min-width: 15em; >+ background-color: white; >+ box-shadow: 0 0 0.5em rgb(0 0 0 / 10%); >+ margin: 0; >+ padding: 0; >+ list-style-type: none; >+ overflow: hidden; >+ border-radius: 0.25em; >+} >+ >+.formkit-dropdown-item[data-selected="true"] { >+ background-color: #cfe8fc; >+} >+ >+.formkit-dropdown-item { >+ padding: 0.5em; >+ border-bottom: 1px solid #e4e4e4; >+} >+ >+a.cancel { >+ cursor: pointer; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue >new file mode 100644 >index 0000000000..bb98c147e7 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue >@@ -0,0 +1,53 @@ >+<template> >+ <div :id="id"> >+ <div id="sub-header"> >+ <Breadcrumbs :title="title"></Breadcrumbs> >+ <Help /> >+ </div> >+ <div class="main container-fluid"> >+ <div class="row"> >+ <template v-if="leftMenu"> >+ <div class="col-sm-10 col-sm-push-2"> >+ <Dialog></Dialog> >+ <slot></slot> >+ </div> >+ <div class="col-sm-2 col-sm-pull-10"> >+ <LeftMenu :title="title"></LeftMenu> >+ </div> >+ </template> >+ <template v-else> >+ <div class="col-sm-12"> >+ <Dialog></Dialog> >+ <slot></slot> >+ </div> >+ </template> >+ </div> >+ </div> >+ </div> >+</template> >+ >+<script> >+import LeftMenu from "./LeftMenu.vue" >+import Breadcrumbs from "./Breadcrumbs.vue" >+import Help from "./Help.vue" >+import Dialog from "./Dialog.vue" >+export default { >+ name: "Page", >+ data: () => ({ >+ leftMenu: true, >+ }), >+ components: { >+ LeftMenu, >+ Dialog, >+ Breadcrumbs, >+ Help, >+ }, >+ props: { >+ id: String, >+ title: String, >+ leftMenu: Boolean, >+ }, >+} >+</script> >+ >+<style></style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue >new file mode 100644 >index 0000000000..167d36d90e >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue >@@ -0,0 +1,80 @@ >+<template> >+ <h1>{{ title }}</h1> >+ <Form >+ :schema="schema" >+ :data="data" >+ @submit="processSubmit" >+ @cancel="doCancel" >+ :loading="loading" >+ ></Form> >+</template> >+ >+<script> >+import Form from "../Form.vue" >+import { inject } from "vue" >+import { ImportSourcesClient } from "../../fetch/import-sources" >+ >+export default { >+ name: "ISEdit", >+ setup() { >+ const { getFormSchema } = inject("modelStore") >+ const api = new ImportSourcesClient() >+ const { setMessage } = inject("mainStore") >+ const schema = getFormSchema() >+ return { >+ schema, >+ setMessage, >+ api, >+ } >+ }, >+ data() { >+ return { >+ data: null, >+ loading: true, >+ } >+ }, >+ methods: { >+ async processSubmit(data) { >+ let response >+ let responseMessage >+ if (data.import_source_id) { >+ const { import_source_id: id, ...row } = data >+ response = await this.api.update({ id, row }) >+ responseMessage = this.$__("Import source updated!") >+ } else { >+ response = await this.api.add({ row: data }) >+ responseMessage = this.$__("Import source created!") >+ } >+ if (response) { >+ this.setMessage(responseMessage) >+ return this.$router.push({ path: "../import-sources.pl" }) >+ } >+ }, >+ doCancel() { >+ this.$router.push({ path: "../import-sources.pl" }) >+ }, >+ }, >+ async created() { >+ const { id } = this.$route.params >+ if (id !== undefined) { >+ const response = await this.api.getOne({ >+ id, >+ }) >+ this.data = response >+ } >+ this.loading = false >+ }, >+ computed: { >+ title() { >+ if (!this.data) return this.$__("Add import source") >+ return this.$__("Edit %s").format(this.data.name) >+ }, >+ }, >+ beforeMount() { >+ this.$root.setTitle(this.title) >+ }, >+ components: { >+ Form, >+ }, >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue >new file mode 100644 >index 0000000000..f649af949b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue >@@ -0,0 +1,100 @@ >+<template> >+ <div id="toolbar" class="btn-toolbar"> >+ <div class="btn-group"> >+ <button class="btn btn-default" @click="newImportSource"> >+ <i class="fa fa-plus"></i> New import source >+ </button> >+ </div> >+ </div> >+ <h1>{{ title }}</h1> >+ <KohaTable >+ v-bind="tableOptions" >+ @edit="doEdit" >+ @delete="doRemove" >+ ></KohaTable> >+</template> >+ >+<script> >+import { useRouter } from "vue-router" >+import { inject } from "vue" >+import { ImportSourcesClient } from "../../fetch/import-sources" >+import KohaTable from "../KohaTable.vue" >+export default { >+ name: "ISList", >+ data() { >+ return { >+ title: this.$__("Import Sources"), >+ tableOptions: { >+ columns: this.columns, >+ options: { >+ embed: "patron", >+ }, >+ actions: { >+ "-1": ["edit", "delete"], >+ }, >+ url: "/api/v1/import_sources", >+ }, >+ } >+ }, >+ setup() { >+ const { getTableColumns } = inject("modelStore") >+ const { setWarning, setMessage, setError, setConfirmationDialog } = >+ inject("mainStore") >+ const api = new ImportSourcesClient() >+ const columns = getTableColumns() >+ return { >+ columns, >+ setWarning, >+ setMessage, >+ setError, >+ setConfirmationDialog, >+ api, >+ } >+ }, >+ beforeMount() { >+ this.$root.setTitle(this.title) >+ }, >+ methods: { >+ newImportSource() { >+ this.$router.push({ path: "import-sources.pl/add" }) >+ }, >+ doEdit(data) { >+ this.$router.push({ >+ path: `import-sources.pl/${data.import_source_id}`, >+ props: { >+ data, >+ }, >+ }) >+ }, >+ async doRemove(data, dt) { >+ this.setConfirmationDialog( >+ { >+ title: this.$__( >+ "Are you sure you want to remove this import source?" >+ ), >+ message: data.name, >+ accept_label: this.$__("Yes, remove"), >+ cancel_label: this.$__("No, do not remove"), >+ }, >+ async () => { >+ const response = await this.api.remove({ >+ id: data.import_source_id, >+ }) >+ if (response) { >+ this.setMessage( >+ this.$__("Import source %s removed").format( >+ data.name >+ ), >+ true >+ ) >+ dt.draw() >+ } >+ } >+ ) >+ }, >+ }, >+ components: { >+ KohaTable, >+ }, >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue >new file mode 100644 >index 0000000000..51800b8772 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue >@@ -0,0 +1,28 @@ >+<template> >+ <Page :left-menu="false" :title="title"> >+ <router-view></router-view> >+ </Page> >+</template> >+ >+<script> >+import Page from "../Page.vue" >+// import { inject } from "vue" >+export default { >+ name: "ISMain", >+ data() { >+ return { >+ title: "", >+ } >+ }, >+ methods: { >+ setTitle(title) { >+ this.title = title >+ }, >+ }, >+ components: { >+ Page, >+ }, >+} >+</script> >+ >+<style></style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js >new file mode 100644 >index 0000000000..b5f0df93aa >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js >@@ -0,0 +1,40 @@ >+import HttpClient from "./http-client"; >+ >+export class ImportSourcesClient extends HttpClient { >+ constructor() { >+ super({ >+ baseURL: '/api/v1/import_sources', >+ }); >+ } >+ async add({row, headers={}}) { >+ const requestHeaders = {...headers, 'Content-Type': 'application/json'} >+ return this.post({ >+ endpoint: '', >+ headers: requestHeaders, >+ body: JSON.stringify(row) >+ }) >+ } >+ >+ async remove({id, headers={}}) { >+ return this.delete({ >+ endpoint: '/'+id, >+ headers, >+ }) >+ } >+ >+ async update({id, headers={}, row}) { >+ const requestHeaders = {...headers, 'Content-Type': 'application/json'} >+ return this.put({ >+ endpoint: '/'+id, >+ headers: requestHeaders, >+ body: JSON.stringify(row) >+ }) >+ } >+ >+ async getOne({id, headers={}}) { >+ return this.get({ >+ endpoint: '/'+id, >+ headers, >+ }) >+ } >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js >index 2e57e1ea49..755da0bb65 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js >@@ -13,6 +13,10 @@ export class PatronAPIClient extends HttpClient { > this.get({ > endpoint: "patrons/" + id, > }), >+ list: (query) => >+ this.get({ >+ endpoint: "patrons" + (query ? '?'+query:''), >+ }) > }; > } > } >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js b/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js >index 8dc89f12d1..93e9d08356 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js >@@ -6,4 +6,4 @@ export default { > install: (app, options) => { > app.config.globalProperties.$__ = $__ > }, >-}; >+}; >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js b/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js >new file mode 100644 >index 0000000000..8fd05b5ce5 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js >@@ -0,0 +1,12 @@ >+import { createWebHistory, createRouter } from "vue-router"; >+ >+export default (menuStore) => { >+ >+ const { routes } = menuStore >+ const router = createRouter({ >+ history: createWebHistory(), >+ linkExactActiveClass: "current", >+ routes, >+ }) >+ return router >+} >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js >new file mode 100644 >index 0000000000..5333b6e73b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js >@@ -0,0 +1,199 @@ >+import { createInput } from '@formkit/vue' >+ >+/** >+ * This is an input "feature" â a function that accepts a node and exposes >+ * some additional functionality to an input. When using schemas, this can >+ * take the place of a traditional "script" block in a Vue component. In this >+ * example, we expose: >+ * >+ * - An input handler `search`. >+ * - An input handler `selections`. >+ * - Commit middleware to place filtered options into the `matches` prop. >+ * >+ * Once written, input features are added via the input declaration. >+ */ >+const searchFeature = (node) => { >+ >+ const _transform_options = (options) => { >+ if(!Array.isArray(options)) { >+ return Object.keys(options).map((key) => ({label: options[key], value: key})) >+ } >+ return options.map((option) => { >+ if(typeof option === 'string') return {label: option, value: option} >+ return option >+ }) >+ } >+ >+ // We wait for our node to be fully "created" before we start to add our >+ // handlers to ensure the core Vue plugin has added its context object: >+ node.on('created', async () => { >+ // Ensure our matches prop starts as an array. >+ node.props.matches = [] >+ node.props.selection = '' >+ >+ if(node.value !== undefined && node.props.attrs && typeof node.props.attrs.optionLoader === 'function') { >+ node.props.selection = await node.props.attrs.optionLoader(node.value) >+ } >+ >+ const clearValue = async (e) => { >+ if (e && typeof e.preventDefault === 'function') e.preventDefault() >+ node.input('') >+ node.props.matches = [] >+ node.props.selection = '' >+ node.props.searchValue = '' >+ setTimeout(() => { >+ if (document.querySelector('input#' + node.props.id)) { >+ document.querySelector('input#' + node.props.id).focus() >+ } >+ }, 50) >+ } >+ // When we actually have an value to set: >+ const setValue = async (e) => { >+ if (e && typeof e.preventDefault === 'function') e.preventDefault() >+ node.input(node.props.selection.value) >+ node.props.display = node.props.selection.label >+ node.props.searchValue = '' >+ await new Promise((r) => setTimeout(r, 50)) // "next tick" >+ } >+ >+ // Perform a soft selection, this is shown as a highlight in the dropdown >+ const select = (delta) => { >+ const available = node.props.matches >+ let idx = available.indexOf(node.props.selection) + delta >+ if (idx >= available.length) { >+ idx = 0 >+ } else if (idx < 0) { >+ idx = available.length - 1 >+ } >+ node.props.selection = available[idx] >+ } >+ >+ // Add some new "handlers" for our autocomplete. The handlers object is >+ // just a conventionally good place to put event handlers. Auto complete >+ // inputs always have to deal with lots of keyboard events, so that logic >+ // is registered here. >+ Object.assign(node.context.handlers, { >+ setValue, >+ clearValue, >+ selection: (e) => { >+ // This handler is called when entering data into the search input. >+ switch (e.key) { >+ case 'Enter': >+ return setValue() >+ case 'ArrowDown': >+ e.preventDefault() >+ return select(1) >+ case 'ArrowUp': >+ e.preventDefault() >+ return select(-1) >+ case 'Escape': >+ return node.props.matches = [] >+ } >+ }, >+ blur: (e) => { >+ if(!node.props.hovering) node.props.matches = [] >+ }, >+ search(e) { >+ node.props.searchValue = e.target.value >+ }, >+ hover: (e) => { >+ node.props.hovering = true >+ node.props.selection = node.props.matches.find(match => String(match.value) === e.target.attributes.value.value) >+ console.log(node.props.selection) >+ }, >+ unhover: (e) => { >+ if (node.props.matches.find(match => String(match.value) === e.target.attributes.value.value) === node.props.selection) { >+ node.props.selection = '' >+ node.props.hovering = false >+ } >+ }, >+ }) >+ }) >+ >+ // Perform filtering when the search value changes >+ node.on('prop:searchValue', async ({ payload: value }) => { >+ let results >+ if(typeof node.props.options === 'function') { >+ results = _transform_options(await node.props.options(value)) >+ } else { >+ results = _transform_options(node.props.options).filter((option) => >+ option.label.toLowerCase().split(/\W/).find((part).startsWith(value.toLowerCase())) >+ ) >+ } >+ //if (!results.length) results.push('No matches') >+ node.props.matches = results >+ }) >+} >+ >+/** >+ * This is our input schema responsible for rendering the inner âinputâ >+ * section. In our example, we render an text input which will be used >+ * to filter search results, and an unordered list that shows all remaining >+ * matches. >+ */ >+const schema = { >+ if: '$value', >+ then: [ >+ { >+ $el: 'a', >+ attrs: { >+ id: '$id', >+ href: '#', >+ class: '$classes.value', >+ onClick: '$handlers.clearValue', >+ }, >+ children: '$selection.label' >+ } >+ ], >+ else: [ >+ { >+ $el: 'input', >+ bind: '$attrs', >+ attrs: { >+ id: '$id', >+ class: '$classes.input', >+ onKeydown: '$handlers.selection', >+ onInput: '$handlers.search', >+ onBlur: '$handlers.blur', >+ value: '$searchValue', >+ }, >+ }, >+ { >+ $el: 'ul', >+ if: '$matches.length', >+ attrs: { >+ class: '$classes.dropdown', >+ }, >+ children: [ >+ { >+ $el: 'li', >+ for: ['match', '$matches'], >+ attrs: { >+ 'data-selected': { >+ if: '$selection === $match', >+ then: 'true', >+ else: 'false', >+ }, >+ value: '$match.value', >+ class: '$classes.dropdownItem', >+ onClick: '$handlers.setValue', >+ onMouseenter: '$handlers.hover', >+ onMouseleave: '$handlers.unhover', >+ }, >+ children: '$match.label', >+ }, >+ ], >+ }, >+ ], >+} >+ >+/** >+ * Finally we create our actual input declaration by using `createInput` this >+ * places our schema into a "standard" FormKit schema feature set with slots, >+ * labels, help, messages etc. The return value of this function is a proper >+ * input declaration. >+ */ >+export default createInput(schema, { >+ props: ['options', 'matches', 'selection', 'searchValue', 'value'], >+ features: [searchFeature], >+}) >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js >new file mode 100644 >index 0000000000..fb70888a28 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js >@@ -0,0 +1,5 @@ >+import autocomplete from './autocomplete' >+ >+export default { >+ autocomplete >+} >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js b/koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js >new file mode 100644 >index 0000000000..53d05c409b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js >@@ -0,0 +1,85 @@ >+ >+import PatronAPIClient from "../fetch/patron-api-client"; >+import { $__ } from "../i18n"; >+ >+const api = new PatronAPIClient() >+ >+export default [ >+ { >+ name: 'import_source_id', >+ table: { >+ data: 'import_source_id', >+ visible: false >+ }, >+ form: { >+ type: 'hidden' >+ } >+ }, >+ { >+ name: 'name', >+ title: $__('Source name'), >+ table: { >+ data: 'name', >+ searchable: true >+ }, >+ form: { >+ type: 'text', >+ label: { >+ $ref: 'title' >+ }, >+ validation: 'required' >+ } >+ }, >+ { >+ name: 'patron_id', >+ title: $__('Patron'), >+ table: { >+ searchable: true, >+ data: "patron.firstname:patron.surname:patron.middle_name", >+ render: (data, type, row) => { >+ const {firstname, surname, middle_name} = row.patron >+ return [firstname, middle_name, surname].filter(part => (part||'').trim()).join(' '); >+ } >+ }, >+ form: { >+ type: 'autocomplete', >+ label: 'Choose a patron', >+ validation: 'required', >+ optionLoader: async (id) => { >+ const {firstname, surname, middle_name, patron_id} = ( await api.patrons.get(id) || {} ); >+ return {value: patron_id, label: [firstname, middle_name, surname].filter(part => (part||'').trim()).join(' ')} >+ }, >+ options: async (value) => { >+ let query = ''; >+ if(value !== undefined) { >+ query = new URLSearchParams({ >+ q: JSON.stringify([ >+ { >+ firstname: { >+ like: '%'+value+'%' >+ } >+ }, >+ { >+ middle_name: { >+ like: '%'+value+'%' >+ } >+ }, >+ { >+ surname: { >+ like: '%'+value+'%' >+ } >+ } >+ ]) >+ }) >+ } >+ >+ const response = await api.patrons.list(query) >+ if(!response) return [] >+ return response.map(row => { >+ const {firstname, surname, middle_name, patron_id} = row >+ return {value: patron_id, label: [firstname, middle_name, surname].filter(part => (part||'').trim()).join(' ')} >+ }) >+ } >+ } >+ } >+] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts >new file mode 100644 >index 0000000000..2ed204e77f >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts >@@ -0,0 +1,76 @@ >+import { createApp } from "vue"; >+import { createPinia } from "pinia"; >+import { plugin as formkit, defaultConfig } from '@formkit/vue' >+import { createWebHistory, createRouter } from "vue-router"; >+import '@formkit/themes/genesis' >+ >+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 { useModelStore } from '../stores/model'; >+import routesDef from '../routes/import-sources'; >+import model from '../models/import-sources'; >+ >+library.add(faPlus, faMinus, faPencil, faTrash, faSpinner); >+ >+const pinia = createPinia(); >+const navigationStore = useNavigationStore(pinia); >+const mainStore = useMainStore(pinia); >+const modelStore = useModelStore(pinia); >+const { removeMessages } = mainStore; >+const { setRoutes } = navigationStore; >+const { setModel } = modelStore; >+const routes = setRoutes(routesDef); >+setModel(model); >+ >+const router = createRouter({ >+ history: createWebHistory(), >+ linkExactActiveClass: "current", >+ routes, >+}) >+ >+import App from "../components/import-sources/ISMain.vue"; >+ >+// import { routes } from "./routes"; >+ >+// const router = createRouter({ >+// history: createWebHistory(), >+// linkExactActiveClass: "current", >+// routes, >+// }); >+ >+// import { useAVStore } from "../stores/authorised_values"; >+ >+import i18n from "../i18n" >+import inputs from "../inputs" >+const app = createApp(App); >+ >+const rootComponent = app >+ .use(i18n) >+ .use(pinia) >+ .use(router) >+ .use(formkit, defaultConfig({ >+ inputs >+ })) >+ .component("font-awesome-icon", FontAwesomeIcon) >+ .component("v-select", vSelect); >+ >+app.config.unwrapInjectedRef = true; >+app.provide("mainStore", mainStore); >+app.provide("navigationStore", navigationStore); >+app.provide("modelStore", modelStore); >+app.mount("#import-source"); >+ >+router.beforeEach((to, from) => { >+ navigationStore.$patch({current: to.meta.self}); >+ removeMessages(); // This will actually flag the messages as displayed already >+}); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js >new file mode 100644 >index 0000000000..471dc59472 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js >@@ -0,0 +1,31 @@ >+import ISEdit from '../components/import-sources/ISEdit.vue' >+import ISList from '../components/import-sources/ISList.vue' >+ >+export default { >+ title: 'Administration', >+ href: '/cgi-bin/koha/admin/admin-home.pl', >+ children: [ >+ { >+ title: 'Import sources', >+ path: '/cgi-bin/koha/admin/import-sources.pl', >+ component: {template: '<router-view></router-view>', name: 'ISBase'}, >+ children: [ >+ { >+ title: 'List', >+ path: '', >+ component: ISList >+ }, >+ { >+ title: 'Add import source', >+ path: 'add', >+ component: ISEdit >+ }, >+ { >+ title: 'Edit import source', >+ path: ':id', >+ component: ISEdit >+ } >+ ] >+ } >+ ] >+} >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js >new file mode 100644 >index 0000000000..f0ccd58acd >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js >@@ -0,0 +1,51 @@ >+import { defineStore } from "pinia"; >+ >+export const useModelStore = defineStore("model", { >+ state: () => ({ >+ model: [] >+ }), >+ actions: { >+ setModel(model) { >+ this.model = model >+ }, >+ getFormSchema() { >+ return this.model.map(row => { >+ const {table, form = {}, ...common} = row >+ Object.keys(form).forEach(key => { >+ if (key === 'type') { >+ form.$formkit = form[key] >+ key = '$formkit' >+ delete form.type; >+ } >+ if(form[key].$ref) { >+ const path = form[key].$ref.split('.') >+ form[key] = path.reduce((obj, key, idx) => { >+ if(obj === undefined || obj[key] === undefined) return undefined >+ const value = obj[key] >+ if(idx === path.lenght - 1) delete obj[key] >+ return value >+ }, common) >+ } >+ }) >+ return {...common, ...form} >+ }) >+ }, >+ getTableColumns() { >+ return this.model.map(row => { >+ const {table = {}, form, ...common} = row >+ Object.keys(table).forEach(key => { >+ if(table[key].$ref) { >+ const path = table[key].$ref.split('.') >+ table[key] = path.reduce((obj, key, idx) => { >+ if(obj === undefined || obj[key] === undefined) return undefined >+ const value = obj[key] >+ if(idx === path.lenght - 1) delete obj[key] >+ return value >+ }, common) >+ } >+ }) >+ return {...common, ...table} >+ }) >+ } >+ }, >+}); >diff --git a/package.json b/package.json >index 1a4d20e98e..940b59af81 100644 >--- a/package.json >+++ b/package.json >@@ -9,6 +9,8 @@ > "dependencies": { > "@cypress/vue": "^3.1.1", > "@cypress/webpack-dev-server": "^1.8.3", >+ "@formkit/themes": "^1.0.0-beta.14", >+ "@formkit/vue": "^1.0.0-beta.14", > "@fortawesome/fontawesome-svg-core": "^6.1.0", > "@fortawesome/free-solid-svg-icons": "^6.0.0", > "@fortawesome/vue-fontawesome": "^3.0.0-5", >diff --git a/t/db_dependent/Koha/ImportSources.t b/t/db_dependent/Koha/ImportSources.t >new file mode 100755 >index 0000000000..6788dab4ee >--- /dev/null >+++ b/t/db_dependent/Koha/ImportSources.t >@@ -0,0 +1,44 @@ >+#!/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 Test::More tests => 1; >+ >+use Koha::ImportSources; >+ >+use t::lib::TestBuilder; >+my $schema = Koha::Database->new->schema; >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'patron' => sub { >+ plan tests => 1; >+ >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object({class => 'Koha::Patrons', value => {firstname => 'Afirstname'}}); >+ >+ my $import_source = $builder->build_object({class => 'Koha::ImportSources', value => {name => 'import_source', patron_id => $patron->borrowernumber}}); >+ >+ my $fetched_patron = $import_source->patron; >+ >+ is($fetched_patron->firstname, 'Afirstname', 'Patron matches'); >+ >+ $schema->storage->txn_rollback; >+}; >\ No newline at end of file >diff --git a/t/db_dependent/api/v1/import_sources.t b/t/db_dependent/api/v1/import_sources.t >new file mode 100755 >index 0000000000..2175a20754 >--- /dev/null >+++ b/t/db_dependent/api/v1/import_sources.t >@@ -0,0 +1,318 @@ >+#!/usr/bin/env 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 Test::More tests => 5; >+use Test::Mojo; >+use Test::Warn; >+ >+use t::lib::TestBuilder; >+use t::lib::Mocks; >+ >+use C4::Auth; >+use Koha::ImportSources; >+use Koha::Database; >+ >+my $schema = Koha::Database->new->schema; >+my $builder = t::lib::TestBuilder->new; >+ >+t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); >+ >+my $t = Test::Mojo->new('Koha::REST::V1'); >+ >+subtest 'list() tests' => sub { >+ >+ plan tests => 12; >+ >+ $schema->storage->txn_begin; >+ >+ my $source = $builder->build_object( >+ { >+ class => 'Koha::ImportSources' >+ } >+ ); >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 3 } >+ } >+ ); >+ >+ for ( 1..10 ) { >+ $builder->build_object( >+ { >+ class => 'Koha::ImportSources' >+ } >+ ); >+ } >+ >+ my $nonprivilegedpatron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 0 } >+ } >+ ); >+ >+ my $password = 'thePassword123'; >+ >+ $nonprivilegedpatron->set_password( >+ { password => $password, skip_validation => 1 } ); >+ my $userid = $nonprivilegedpatron->userid; >+ >+ $t->get_ok( "//$userid:$password@/api/v1/import_sources" ) >+ ->status_is(403) >+ ->json_is( >+ '/error' => 'Authorization failure. Missing required permission(s).' ); >+ >+ $patron->set_password( { password => $password, skip_validation => 1 } ); >+ $userid = $patron->userid; >+ >+ $t->get_ok( "//$userid:$password@/api/v1/import_sources?_per_page=10" ) >+ ->status_is( 200, 'SWAGGER3.2.2' ); >+ >+ my $response_count = scalar @{ $t->tx->res->json }; >+ >+ is( $response_count, 10, 'The API returns 10 sources' ); >+ >+ my $id = $source->import_source_id; >+ $t->get_ok( "//$userid:$password@/api/v1/import_sources?q={\"import_source_id\": $id}" ) >+ ->status_is(200) >+ ->json_is( '' => [ $source->to_api ], 'SWAGGER3.3.2'); >+ >+ $source->delete; >+ >+ $t->get_ok( "//$userid:$password@/api/v1/import_sources?q={\"import_source_id\": $id}" ) >+ ->status_is(200) >+ ->json_is( '' => [] ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'get() tests' => sub { >+ >+ plan tests => 9; >+ >+ $schema->storage->txn_begin; >+ >+ my $source = $builder->build_object( >+ { >+ class => 'Koha::ImportSources' >+ } >+ ); >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 3 } >+ } >+ ); >+ >+ my $nonprivilegedpatron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 0 } >+ } >+ ); >+ >+ my $password = 'thePassword123'; >+ >+ $nonprivilegedpatron->set_password( >+ { password => $password, skip_validation => 1 } ); >+ my $userid = $nonprivilegedpatron->userid; >+ >+ my $id = $source->import_source_id; >+ >+ $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" ) >+ ->status_is(403) >+ ->json_is( >+ '/error' => 'Authorization failure. Missing required permission(s).' ); >+ >+ $patron->set_password( { password => $password, skip_validation => 1 } ); >+ $userid = $patron->userid; >+ >+ $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" ) >+ ->status_is( 200, 'SWAGGER3.2.2' ) >+ ->json_is( '' => $source->to_api, 'SWAGGER3.3.2' ); >+ >+ $source->delete; >+ >+ $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" ) >+ ->status_is(404) >+ ->json_is( '/error' => 'Import source not found' ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'delete() tests' => sub { >+ >+ plan tests => 6; >+ >+ $schema->storage->txn_begin; >+ >+ my $source = $builder->build_object( >+ { >+ class => 'Koha::ImportSources' >+ } >+ ); >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 3 } >+ } >+ ); >+ >+ my $nonprivilegedpatron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 0 } >+ } >+ ); >+ >+ my $password = 'thePassword123'; >+ >+ $nonprivilegedpatron->set_password( >+ { password => $password, skip_validation => 1 } ); >+ my $userid = $nonprivilegedpatron->userid; >+ >+ my $id = $source->import_source_id; >+ >+ $t->delete_ok( "//$userid:$password@/api/v1/import_sources/$id" ) >+ ->status_is(403) >+ ->json_is( >+ '/error' => 'Authorization failure. Missing required permission(s).' ); >+ >+ $patron->set_password( { password => $password, skip_validation => 1 } ); >+ $userid = $patron->userid; >+ >+ $t->delete_ok( "//$userid:$password@/api/v1/import_sources/$id" ) >+ ->status_is( 204, 'SWAGGER3.2.2' ); >+ >+ my $deleted_source = Koha::ImportSources->search({import_source_id => $id}); >+ >+ is($deleted_source->count, 0, 'No import source found'); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'add() tests' => sub { >+ >+ plan tests => 8; >+ >+ $schema->storage->txn_begin; >+ >+ Koha::ImportSources->delete; >+ >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 3 } >+ } >+ ); >+ >+ my $nonprivilegedpatron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 0 } >+ } >+ ); >+ >+ my $password = 'thePassword123'; >+ >+ $nonprivilegedpatron->set_password( >+ { password => $password, skip_validation => 1 } ); >+ my $userid = $nonprivilegedpatron->userid; >+ my $patron_id = $nonprivilegedpatron->borrowernumber; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/import_sources" => json => { name => 'test1', patron_id => $patron_id }) >+ ->status_is(403) >+ ->json_is( >+ '/error' => 'Authorization failure. Missing required permission(s).' ); >+ >+ $patron->set_password( { password => $password, skip_validation => 1 } ); >+ $userid = $patron->userid; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/import_sources" => json => { name => 'test1', patron_id => $patron_id }) >+ ->status_is( 201, 'SWAGGER3.2.2' ) >+ ->json_is('/name', 'test1') >+ ->json_is('/patron_id', $patron_id); >+ >+ my $created_source = Koha::ImportSources->search->next; >+ >+ is($created_source->name, 'test1', 'Import source found'); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'update() tests' => sub { >+ >+ plan tests => 7; >+ >+ $schema->storage->txn_begin; >+ >+ my $source = $builder->build_object( >+ { >+ class => 'Koha::ImportSources', >+ value => { name => 'Oldname' } >+ } >+ ); >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 3 } >+ } >+ ); >+ >+ my $nonprivilegedpatron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 0 } >+ } >+ ); >+ >+ my $password = 'thePassword123'; >+ >+ $nonprivilegedpatron->set_password( >+ { password => $password, skip_validation => 1 } ); >+ my $userid = $nonprivilegedpatron->userid; >+ >+ my $id = $source->import_source_id; >+ >+ $t->put_ok( "//$userid:$password@/api/v1/import_sources/$id" => json => { name => 'Newname', patron_id => $source->patron_id } ) >+ ->status_is(403) >+ ->json_is( >+ '/error' => 'Authorization failure. Missing required permission(s).' ); >+ >+ $patron->set_password( { password => $password, skip_validation => 1 } ); >+ $userid = $patron->userid; >+ >+ $t->put_ok( "//$userid:$password@/api/v1/import_sources/$id" => json => { name => 'Newname', patron_id => $source->patron_id } ) >+ ->status_is( 200, 'SWAGGER3.2.2' ) >+ ->json_is('/name', 'Newname'); >+ >+ my $updated_source = Koha::ImportSources->find($id); >+ >+ is($updated_source->name, 'Newname', 'Import source updated'); >+ >+ $schema->storage->txn_rollback; >+ >+}; >\ No newline at end of file >diff --git a/webpack.config.js b/webpack.config.js >index bbdcced844..5a6cff9fd7 100644 >--- a/webpack.config.js >+++ b/webpack.config.js >@@ -6,6 +6,7 @@ const webpack = require('webpack'); > module.exports = { > entry: { > erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts", >+ "import-source": "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts" > }, > output: { > filename: "[name].js", >-- >2.25.1
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 32607
:
146410
|
146411
|
146412
|
149777
|
149827
|
149828
|
149829
|
154006
|
154007
|
154008
|
154009
|
154010
|
154438
|
155682
|
155683
|
155684
|
155685
|
155686
|
155958
|
157756
|
157757
|
157758
|
157759
|
157760
|
157761
|
157762
|
161362
|
161363
|
161364
|
161365
|
161366
|
161367
|
161435
|
161436
|
161437
|
161438
|
161439
|
161440
|
161441
|
161474