View | Details | Raw Unified | Return to bug 32607
Collapse All | Expand All

(-)a/Koha/ImportSource.pm (+56 lines)
Line 0 Link Here
1
package Koha::ImportSource;
2
3
# This file is part of Koha.
4
#
5
# Copyright 2020 Koha Development Team
6
#
7
# Koha is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as
9
# published by the Free Software Foundation; either version 3
10
# of the License, or (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General
18
# Public License along with Koha; if not, see
19
# <http://www.gnu.org/licenses>
20
21
use Modern::Perl;
22
23
use base qw(Koha::Object);
24
25
=head1 NAME
26
27
Koha::ImportSource - Koha ImportSource Object class
28
29
=head1 API
30
31
=head2 Class Methods
32
33
=head3 patron
34
35
my $patron = $import_source->patron
36
37
Return the patron for this import source
38
39
=cut
40
41
sub patron {
42
    my ($self) = @_;
43
44
    my $patron_rs = $self->_result->patron;
45
    return Koha::Patron->_new_from_dbic($patron_rs);
46
}
47
48
=head3 _type
49
50
=cut
51
52
sub _type {
53
    return 'ImportSource';
54
}
55
56
1;
(-)a/Koha/ImportSources.pm (+51 lines)
Line 0 Link Here
1
package Koha::ImportSources;
2
3
# This file is part of Koha.
4
#
5
# Copyright 2020 Koha Development Team
6
#
7
# Koha is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as
9
# published by the Free Software Foundation; either version 3
10
# of the License, or (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General
18
# Public License along with Koha; if not, see
19
# <http://www.gnu.org/licenses>
20
21
use Modern::Perl;
22
23
use base qw(Koha::Objects);
24
25
use Koha::ImportSource;
26
27
=head1 NAME
28
29
Koha::ImportSources - Koha ImportSources Object class
30
31
=head1 API
32
33
=head2 Class Methods
34
35
=head3 _type
36
37
=cut
38
39
sub _type {
40
    return 'ImportSource';
41
}
42
43
=head3 object_class
44
45
=cut
46
47
sub object_class {
48
    return 'Koha::ImportSource';
49
}
50
51
1;
(-)a/Koha/REST/V1/ImportSources.pm (+141 lines)
Line 0 Link Here
1
package Koha::REST::V1::ImportSources;
2
3
# Copyright 2023 Theke Solutions
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Mojo::Base 'Mojolicious::Controller';
23
24
use Koha::ImportSources;
25
26
use Try::Tiny qw( catch try );
27
28
=head1 API
29
30
=head2 Methods
31
32
=head3 list
33
34
=cut
35
36
sub list {
37
    my $c = shift->openapi->valid_input or return;
38
39
    return try {
40
        my $import_sources_set = Koha::ImportSources->new;
41
        my $import_sources = $c->objects->search( $import_sources_set );
42
        return $c->render( status => 200, openapi => $import_sources );
43
    }
44
    catch {
45
        $c->unhandled_exception($_);
46
    };
47
}
48
49
=head3 get
50
51
=cut
52
53
sub get {
54
    my $c = shift->openapi->valid_input or return;
55
56
    return try {
57
        my $import_sources_set = Koha::ImportSources->new;
58
        my $import_source = $c->objects->find( $import_sources_set, $c->validation->param('import_source_id') );
59
        unless ($import_source) {
60
            return $c->render( status  => 404,
61
                            openapi => { error => "Import source not found" } );
62
        }
63
64
        return $c->render( status => 200, openapi => $import_source );
65
    }
66
    catch {
67
        $c->unhandled_exception($_);
68
    };
69
}
70
71
=head3 add
72
73
=cut
74
75
sub add {
76
    my $c = shift->openapi->valid_input or return;
77
78
    return try {
79
        my $import_source = Koha::ImportSource->new_from_api( $c->validation->param('body') );
80
        $import_source->store;
81
        $c->res->headers->location( $c->req->url->to_string . '/' . $import_source->import_source_id );
82
        return $c->render(
83
            status  => 201,
84
            openapi => $import_source->to_api
85
        );
86
    }
87
    catch {
88
        $c->unhandled_exception($_);
89
    };
90
}
91
92
=head3 update
93
94
=cut
95
96
sub update {
97
    my $c = shift->openapi->valid_input or return;
98
99
    my $import_source = Koha::ImportSources->find( $c->validation->param('import_source_id') );
100
101
    if ( not defined $import_source ) {
102
        return $c->render( status  => 404,
103
                           openapi => { error => "Object not found" } );
104
    }
105
106
    return try {
107
        $import_source->set_from_api( $c->validation->param('body') );
108
        $import_source->store();
109
        return $c->render( status => 200, openapi => $import_source->to_api );
110
    }
111
    catch {
112
        $c->unhandled_exception($_);
113
    };
114
}
115
116
=head3 delete
117
118
=cut
119
120
sub delete {
121
    my $c = shift->openapi->valid_input or return;
122
123
    my $import_source = Koha::ImportSources->find( $c->validation->param('import_source_id') );
124
    if ( not defined $import_source ) {
125
        return $c->render( status  => 404,
126
                           openapi => { error => "Object not found" } );
127
    }
128
129
    return try {
130
        $import_source->delete;
131
        return $c->render(
132
            status  => 204,
133
            openapi => q{}
134
        );
135
    }
136
    catch {
137
        $c->unhandled_exception($_);
138
    };
139
}
140
141
1;
(-)a/Koha/Schema/Result/ImportSource.pm (+8 lines)
Lines 86-89 __PACKAGE__->belongs_to( Link Here
86
86
87
87
88
# You can replace this text with custom code or comments, and it will be preserved on regeneration
88
# You can replace this text with custom code or comments, and it will be preserved on regeneration
89
90
sub koha_object_class {
91
    'Koha::ImportSource';
92
}
93
sub koha_objects_class {
94
    'Koha::ImportSources';
95
}
96
89
1;
97
1;
(-)a/admin/import-sources.pl (+38 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2023 Theke Solutions
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use CGI qw ( -utf8 );
23
use C4::Auth qw( get_template_and_user );
24
use C4::Output qw( output_html_with_http_headers );
25
use Koha::Plugins;
26
27
my $query = CGI->new;
28
29
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
30
    {
31
        template_name   => "admin/import-sources.tt",
32
        query           => $query,
33
        type            => "intranet",
34
        flagsrequired   => { parameters => 'manage_import_sources' },
35
    }
36
);
37
38
output_html_with_http_headers $query, $cookie, $template->output;
(-)a/api/v1/swagger/definitions/import_source.yaml (+13 lines)
Line 0 Link Here
1
---
2
type: object
3
properties:
4
  import_source_id:
5
    type: integer
6
    description: internally assigned import source identifier
7
    readOnly: true
8
  name:
9
    description: import source name
10
    type: string
11
  patron_id:
12
    description: linked patron identifier
13
    type: integer
(-)a/api/v1/swagger/paths/import_sources.yaml (+237 lines)
Line 0 Link Here
1
/import_sources:
2
  get:
3
    x-mojo-to: ImportSources#list
4
    operationId: listImportSources
5
    summary: List import sources
6
    tags:
7
      - import_sources
8
    parameters:
9
      - $ref: "../swagger.yaml#/parameters/match"
10
      - $ref: "../swagger.yaml#/parameters/order_by"
11
      - $ref: "../swagger.yaml#/parameters/page"
12
      - $ref: "../swagger.yaml#/parameters/per_page"
13
      - $ref: "../swagger.yaml#/parameters/q_param"
14
      - $ref: "../swagger.yaml#/parameters/q_body"
15
      - $ref: "../swagger.yaml#/parameters/q_header"
16
      - $ref: "../swagger.yaml#/parameters/request_id_header"
17
    consumes:
18
      - application/json
19
    produces:
20
      - application/json
21
    responses:
22
      "200":
23
        description: A list of import sources
24
      "400":
25
        description: Missing or wrong parameters
26
        schema:
27
          $ref: "../swagger.yaml#/definitions/error"
28
      "401":
29
        description: Authentication required
30
        schema:
31
          $ref: "../swagger.yaml#/definitions/error"
32
      "403":
33
        description: Not allowed
34
        schema:
35
          $ref: "../swagger.yaml#/definitions/error"
36
      "500":
37
        description: |
38
          Internal server error. Possible `error_code` attribute values:
39
40
          * `internal_server_error`
41
        schema:
42
          $ref: "../swagger.yaml#/definitions/error"
43
      "503":
44
        description: Under maintenance
45
        schema:
46
          $ref: "../swagger.yaml#/definitions/error"
47
    x-koha-authorization:
48
      permissions:
49
        parameters: manage_import_sources
50
  post:
51
    x-mojo-to: ImportSources#add
52
    operationId: addImportSources
53
    summary: Add an import source
54
    tags:
55
      - import_sources
56
    parameters:
57
      - name: body
58
        in: body
59
        description: A JSON object containing informations about the new import source
60
        required: true
61
        schema:
62
          $ref: "../swagger.yaml#/definitions/import_source"
63
    consumes:
64
      - application/json
65
    produces:
66
      - application/json
67
    responses:
68
      "201":
69
        description: An import source
70
      "400":
71
        description: Missing or wrong parameters
72
        schema:
73
          $ref: "../swagger.yaml#/definitions/error"
74
      "401":
75
        description: Authentication required
76
        schema:
77
          $ref: "../swagger.yaml#/definitions/error"
78
      "403":
79
        description: Not allowed
80
        schema:
81
          $ref: "../swagger.yaml#/definitions/error"
82
      "500":
83
        description: |
84
          Internal server error. Possible `error_code` attribute values:
85
86
          * `internal_server_error`
87
        schema:
88
          $ref: "../swagger.yaml#/definitions/error"
89
      "503":
90
        description: Under maintenance
91
        schema:
92
          $ref: "../swagger.yaml#/definitions/error"
93
    x-koha-authorization:
94
      permissions:
95
        parameters: manage_import_sources
96
"/import_sources/{import_source_id}":
97
  get:
98
    x-mojo-to: ImportSources#get
99
    operationId: getImportSources
100
    summary: Get an import source
101
    tags:
102
      - import_sources
103
    parameters:
104
      - $ref: "../swagger.yaml#/parameters/import_source_id_pp"
105
    consumes:
106
      - application/json
107
    produces:
108
      - application/json
109
    responses:
110
      "200":
111
        description: An import source
112
      "400":
113
        description: Missing or wrong parameters
114
        schema:
115
          $ref: "../swagger.yaml#/definitions/error"
116
      "401":
117
        description: Authentication required
118
        schema:
119
          $ref: "../swagger.yaml#/definitions/error"
120
      "403":
121
        description: Not allowed
122
        schema:
123
          $ref: "../swagger.yaml#/definitions/error"
124
      "404":
125
        description: Not found
126
        schema:
127
          $ref: "../swagger.yaml#/definitions/error"
128
      "500":
129
        description: |
130
          Internal server error. Possible `error_code` attribute values:
131
132
          * `internal_server_error`
133
        schema:
134
          $ref: "../swagger.yaml#/definitions/error"
135
      "503":
136
        description: Under maintenance
137
        schema:
138
          $ref: "../swagger.yaml#/definitions/error"
139
    x-koha-authorization:
140
      permissions:
141
        parameters: manage_import_sources
142
  put:
143
    x-mojo-to: ImportSources#update
144
    operationId: updateImportSources
145
    summary: Update an import source
146
    tags:
147
      - import_sources
148
    parameters:
149
      - $ref: "../swagger.yaml#/parameters/import_source_id_pp"
150
      - name: body
151
        in: body
152
        description: A JSON object containing informations about the new import source
153
        required: true
154
        schema:
155
          $ref: "../swagger.yaml#/definitions/import_source"
156
    consumes:
157
      - application/json
158
    produces:
159
      - application/json
160
    responses:
161
      "200":
162
        description: An import source
163
      "400":
164
        description: Missing or wrong parameters
165
        schema:
166
          $ref: "../swagger.yaml#/definitions/error"
167
      "401":
168
        description: Authentication required
169
        schema:
170
          $ref: "../swagger.yaml#/definitions/error"
171
      "403":
172
        description: Not allowed
173
        schema:
174
          $ref: "../swagger.yaml#/definitions/error"
175
      "404":
176
        description: Not found
177
        schema:
178
          $ref: "../swagger.yaml#/definitions/error"
179
      "500":
180
        description: |
181
          Internal server error. Possible `error_code` attribute values:
182
183
          * `internal_server_error`
184
        schema:
185
          $ref: "../swagger.yaml#/definitions/error"
186
      "503":
187
        description: Under maintenance
188
        schema:
189
          $ref: "../swagger.yaml#/definitions/error"
190
    x-koha-authorization:
191
      permissions:
192
        parameters: manage_import_sources
193
  delete:
194
    x-mojo-to: ImportSources#delete
195
    operationId: deleteImportSources
196
    summary: Delete an import source
197
    tags:
198
      - import_sources
199
    parameters:
200
      - $ref: "../swagger.yaml#/parameters/import_source_id_pp"
201
    consumes:
202
      - application/json
203
    produces:
204
      - application/json
205
    responses:
206
      "204":
207
        description: Deleted
208
      "400":
209
        description: Missing or wrong parameters
210
        schema:
211
          $ref: "../swagger.yaml#/definitions/error"
212
      "401":
213
        description: Authentication required
214
        schema:
215
          $ref: "../swagger.yaml#/definitions/error"
216
      "403":
217
        description: Not allowed
218
        schema:
219
          $ref: "../swagger.yaml#/definitions/error"
220
      "404":
221
        description: Not found
222
        schema:
223
          $ref: "../swagger.yaml#/definitions/error"
224
      "500":
225
        description: |
226
          Internal server error. Possible `error_code` attribute values:
227
228
          * `internal_server_error`
229
        schema:
230
          $ref: "../swagger.yaml#/definitions/error"
231
      "503":
232
        description: Under maintenance
233
        schema:
234
          $ref: "../swagger.yaml#/definitions/error"
235
    x-koha-authorization:
236
      permissions:
237
        parameters: manage_import_sources
(-)a/api/v1/swagger/swagger.yaml (+15 lines)
Lines 54-59 definitions: Link Here
54
    $ref: ./definitions/import_batch_profiles.yaml
54
    $ref: ./definitions/import_batch_profiles.yaml
55
  import_record_match:
55
  import_record_match:
56
    $ref: ./definitions/import_record_match.yaml
56
    $ref: ./definitions/import_record_match.yaml
57
  import_source:
58
    $ref: ./definitions/import_source.yaml
57
  invoice:
59
  invoice:
58
    $ref: ./definitions/invoice.yaml
60
    $ref: ./definitions/invoice.yaml
59
  item:
61
  item:
Lines 235-240 paths: Link Here
235
    $ref: ./paths/import_batch_profiles.yaml#/~1import_batch_profiles
237
    $ref: ./paths/import_batch_profiles.yaml#/~1import_batch_profiles
236
  "/import_batch_profiles/{import_batch_profile_id}":
238
  "/import_batch_profiles/{import_batch_profile_id}":
237
    $ref: "./paths/import_batch_profiles.yaml#/~1import_batch_profiles~1{import_batch_profile_id}"
239
    $ref: "./paths/import_batch_profiles.yaml#/~1import_batch_profiles~1{import_batch_profile_id}"
240
  /import_sources:
241
    $ref: ./paths/import_sources.yaml#/~1import_sources
242
  "/import_sources/{import_source_id}":
243
    $ref: ./paths/import_sources.yaml#/~1import_sources~1{import_source_id}
238
  /items:
244
  /items:
239
    $ref: ./paths/items.yaml#/~1items
245
    $ref: ./paths/items.yaml#/~1items
240
  "/items/{item_id}":
246
  "/items/{item_id}":
Lines 434-439 parameters: Link Here
434
    name: import_record_id
440
    name: import_record_id
435
    required: true
441
    required: true
436
    type: integer
442
    type: integer
443
  import_source_id_pp:
444
    description: Internal import source identifier
445
    in: path
446
    name: import_source_id
447
    required: true
448
    type: integer
437
  item_id_pp:
449
  item_id_pp:
438
    description: Internal item identifier
450
    description: Internal item identifier
439
    in: path
451
    in: path
Lines 724-729 tags: Link Here
724
  - description: "Manage item groups\n"
736
  - description: "Manage item groups\n"
725
    name: item_groups
737
    name: item_groups
726
    x-displayName: Item groups
738
    x-displayName: Item groups
739
  - description: "Manage import sources\n"
740
    name: import_sources
741
    x-displayName: Import source
727
  - description: "Manage items\n"
742
  - description: "Manage items\n"
728
    name: items
743
    name: items
729
    x-displayName: Items
744
    x-displayName: Items
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 211-216 Link Here
211
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></dt>
211
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></dt>
212
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields</dd>
212
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields</dd>
213
                    [% END %]
213
                    [% END %]
214
                    <dt><a href="/cgi-bin/koha/admin/import-sources.pl">Import sources</a></dt>
215
                    <dd>Define sources to import from</dd>
214
                </dl>
216
                </dl>
215
            [% END %]
217
            [% END %]
216
218
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt (+37 lines)
Line 0 Link Here
1
[% USE raw %]
2
[% USE To %]
3
[% USE Asset %]
4
[% USE KohaDates %]
5
[% USE TablesSettings %]
6
[% USE AuthorisedValues %]
7
[% SET footerjs = 1 %]
8
[% PROCESS 'i18n.inc' %]
9
[% INCLUDE 'doc-head-open.inc' %]
10
<title>
11
    Import sources &rsaquo; Koha
12
</title>
13
[% INCLUDE 'doc-head-close.inc' %]
14
</head>
15
16
<body id="erm_agreements" class="erm">
17
[% WRAPPER 'header.inc' %]
18
    [% INCLUDE 'erm-search.inc' %]
19
[% END %]
20
21
<div id="import-source"> <!-- this is closed in intranet-bottom.inc -->
22
23
[% MACRO jsinclude BLOCK %]
24
    [% INCLUDE 'calendar.inc' %]
25
    [% INCLUDE 'datatables.inc' %]
26
    [% INCLUDE 'columns_settings.inc' %]
27
    [% INCLUDE 'js-patron-format.inc' %]
28
    [% INCLUDE 'js-date-format.inc' %]
29
30
    <script>
31
      const RESTOAuth2ClientCredentials = [% IF Koha.Preference('RESTOAuth2ClientCredentials') %]true[% ELSE %]false[% END %];
32
    </script>
33
34
    [% Asset.js("js/vue/dist/import-source.js") | $raw %]
35
36
[% END %]
37
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Dialog.vue (-6 / +14 lines)
Lines 1-8 Link Here
1
<template>
1
<template>
2
    <div class="dialog message" v-if="message">{{ message }}</div>
2
    <div class="dialog message" v-if="message" v-html="message"></div>
3
    <div class="dialog alert" v-if="error">{{ error }}</div>
3
    <div class="dialog alert" v-if="error" v-html="error"></div>
4
    <div class="dialog alert modal" v-if="warning">
4
    <div class="dialog alert modal" v-if="warning">
5
        {{ warning }}
5
        <span v-html="warning"></span>
6
        <a
7
            v-if="accept"
8
            id="close_modal"
9
            class="btn btn-primary btn-xs"
10
            role="button"
11
            @click="accept"
12
            >{{ $__("Accept") }}</a
13
        >
6
        <a
14
        <a
7
            id="close_modal"
15
            id="close_modal"
8
            class="btn btn-default btn-xs"
16
            class="btn btn-default btn-xs"
Lines 20-29 import { storeToRefs } from "pinia" Link Here
20
export default {
28
export default {
21
    setup() {
29
    setup() {
22
        const mainStore = inject("mainStore")
30
        const mainStore = inject("mainStore")
23
        const { message, error, warning } = storeToRefs(mainStore)
31
        const { message, error, warning, accept } = storeToRefs(mainStore)
24
        const { removeMessages } = mainStore
32
        const { removeMessages } = mainStore
25
        return { message, error, warning, removeMessages }
33
        return { message, error, warning, accept, removeMessages }
26
    },
34
    }
27
}
35
}
28
</script>
36
</script>
29
37
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue (+99 lines)
Line 0 Link Here
1
<template>
2
    <h1>{{ title }}</h1>
3
    <Form
4
        :schema="schema"
5
        :data="data"
6
        @submit="processSubmit"
7
        @cancel="doCancel"
8
        :loading="loading"
9
    ></Form>
10
</template>
11
12
<script>
13
import Form from "../shared/Form.vue"
14
import { inject } from "vue"
15
import { useRouter } from "vue-router"
16
17
export default {
18
    name: "ISEdit",
19
    setup() {
20
        const { getFormSchema } = inject("modelStore")
21
        const { add, update, get } = inject("tableStore")
22
        const { setError, setMessage } = inject("mainStore")
23
        const schema = getFormSchema()
24
        const router = useRouter()
25
        return {
26
            schema,
27
            add,
28
            update,
29
            setError,
30
            setMessage,
31
            router,
32
            get,
33
        }
34
    },
35
    data() {
36
        return {
37
            data: null,
38
            loading: true,
39
        }
40
    },
41
    methods: {
42
        async processSubmit(data) {
43
            let response
44
            let responseMessage
45
            if (data.import_source_id) {
46
                const { import_source_id: id, ...row } = data
47
                response = await this.update({ id, row })
48
                responseMessage = this.$__("Import source updated!")
49
            } else {
50
                response = await this.add({ row: data })
51
                responseMessage = this.$__("Import source created!")
52
            }
53
            if (response.ok) {
54
                this.setMessage(responseMessage)
55
                return this.router.push({ path: "../import-sources.pl" })
56
            } else {
57
                this.setError(
58
                    this.$__(
59
                        "Could not create the import source. Error: %s"
60
                    ).format(response.statusText)
61
                )
62
            }
63
        },
64
        doCancel() {
65
            this.router.push({ path: "../import-sources.pl" })
66
        },
67
    },
68
    async created() {
69
        const { id } = this.$route.params
70
        if (id !== undefined) {
71
            const response = await this.get({
72
                id,
73
            })
74
            if (response.ok) {
75
                this.data = response.data
76
            } else {
77
                this.setError(
78
                    this.$__(
79
                        "Could not fetch import source data. Error %s"
80
                    ).format(response.statusText)
81
                )
82
            }
83
        }
84
        this.loading = false
85
    },
86
    computed: {
87
        title() {
88
            if (!this.data) return this.$__("Add import source")
89
            return this.$__("Edit %s").format(this.data.name)
90
        },
91
    },
92
    beforeMount() {
93
        this.$root.setTitle(this.title)
94
    },
95
    components: {
96
        Form,
97
    },
98
}
99
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue (+82 lines)
Line 0 Link Here
1
<template>
2
    <div id="toolbar" class="btn-toolbar">
3
        <div class="btn-group">
4
            <button class="btn btn-default" @click="newImportSource">
5
                <i class="fa fa-plus"></i> New import source
6
            </button>
7
        </div>
8
    </div>
9
    <h1>{{ title }}</h1>
10
    <KohaTable
11
        v-bind="tableOptions"
12
        @edit="doEdit"
13
        @remove="doRemove"
14
    ></KohaTable>
15
</template>
16
17
<script>
18
import { useRouter } from "vue-router"
19
import { inject } from "vue"
20
import KohaTable from "../shared/KohaTable.vue"
21
export default {
22
    name: "ISList",
23
    data() {
24
        return {
25
            title: this.$__("Import Sources"),
26
            tableOptions: {
27
                columns: this.columns,
28
                options: {
29
                    embed: "patron",
30
                },
31
            },
32
        }
33
    },
34
    setup() {
35
        const router = useRouter()
36
        const { getTableColumns } = inject("modelStore")
37
        const { setWarning, setMessage, setError } = inject("mainStore")
38
        const { remove } = inject("tableStore")
39
        const columns = getTableColumns()
40
        return {
41
            router,
42
            columns,
43
            setWarning,
44
            setMessage,
45
            setError,
46
            remove,
47
        }
48
    },
49
    beforeMount() {
50
        this.$root.setTitle(this.title)
51
    },
52
    methods: {
53
        newImportSource() {
54
            this.router.push({ path: "import-sources.pl/add" })
55
        },
56
        doEdit(data) {
57
            this.router.push({
58
                path: `import-sources.pl/${data.import_source_id}`,
59
                props: {
60
                    data,
61
                },
62
            })
63
        },
64
        async doRemove(data, done) {
65
            const response = await this.remove({ id: data.import_source_id })
66
            if (response.ok) {
67
                this.setMessage(this.$__("Import source removed"), true)
68
                done()
69
            } else {
70
                this.setError(
71
                    this.$__(
72
                        "Could not remove import source. Error: %s"
73
                    ).format(response.statusText)
74
                )
75
            }
76
        },
77
    },
78
    components: {
79
        KohaTable,
80
    },
81
}
82
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue (+28 lines)
Line 0 Link Here
1
<template>
2
    <Page :side-menu="false" :title="title">
3
        <router-view></router-view>
4
    </Page>
5
</template>
6
7
<script>
8
import Page from "../shared/Page.vue"
9
import { inject } from "vue"
10
export default {
11
    name: "ISMain",
12
    data() {
13
        return {
14
            title: "",
15
        }
16
    },
17
    methods: {
18
        setTitle(title) {
19
            this.title = title
20
        },
21
    },
22
    components: {
23
        Page,
24
    },
25
}
26
</script>
27
28
<style></style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/shared/Breadcrumbs.vue (+41 lines)
Line 0 Link Here
1
<template>
2
    <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumb">
3
        <ol>
4
            <template v-for="(item, idx) in breadcrumbs" v-bind:key="idx">
5
                <MenuItem
6
                    v-if="idx < breadcrumbs.length - 1"
7
                    :item="item"
8
                ></MenuItem>
9
                <MenuItem
10
                    v-else
11
                    :item="{ ...item, path: undefined, href: undefined }"
12
                ></MenuItem>
13
            </template>
14
        </ol>
15
    </nav>
16
</template>
17
18
<script>
19
import { inject } from "vue"
20
import { storeToRefs } from "pinia"
21
import MenuItem from "./MenuItem.vue"
22
export default {
23
    name: "Breadcrumbs",
24
    setup: () => {
25
        const menuStore = inject("menuStore")
26
        const { breadcrumbs } = storeToRefs(menuStore)
27
        return {
28
            breadcrumbs,
29
        }
30
    },
31
    data: () => ({
32
        mode: "menu",
33
    }),
34
    props: {
35
        title: String,
36
    },
37
    components: {
38
        MenuItem,
39
    },
40
}
41
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/shared/Dialog.vue (+53 lines)
Line 0 Link Here
1
<template>
2
    <div class="dialog message" v-if="message" v-html="message"></div>
3
    <div class="dialog alert" v-if="error" v-html="error"></div>
4
    <div class="dialog alert modal" v-if="warning">
5
        <span v-html="warning"></span>
6
        <a
7
            v-if="accept"
8
            id="close_modal"
9
            class="btn btn-primary btn-xs"
10
            role="button"
11
            @click="accept"
12
            >{{ $__("Accept") }}</a
13
        >
14
        <a
15
            id="close_modal"
16
            class="btn btn-default btn-xs"
17
            role="button"
18
            @click="removeMessages"
19
            >{{ $__("Close") }}</a
20
        >
21
    </div>
22
    <!-- Must be styled differently -->
23
</template>
24
25
<script>
26
import { inject } from "vue"
27
import { storeToRefs } from "pinia"
28
export default {
29
    setup() {
30
        const mainStore = inject("mainStore")
31
        const { message, error, warning, accept } = storeToRefs(mainStore)
32
        const { removeMessages } = mainStore
33
        return { message, error, warning, accept, removeMessages }
34
    }
35
}
36
</script>
37
38
<style scoped>
39
.modal {
40
    position: fixed;
41
    z-index: 9998;
42
    display: table;
43
    transition: opacity 0.3s ease;
44
    margin: auto;
45
    padding: 20px 30px;
46
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
47
    transition: all 0.3s ease;
48
}
49
#close_modal {
50
    float: right;
51
    cursor: pointer;
52
}
53
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/shared/Form.vue (+140 lines)
Line 0 Link Here
1
<template>
2
    <FormKit
3
        v-if="!loading"
4
        ref="form"
5
        @submit="doSubmit"
6
        type="form"
7
        :actions="false"
8
        :value="data"
9
    >
10
        <FormKitSchema :schema="formSchema" :data="schemaData"></FormKitSchema>
11
    </FormKit>
12
    <div v-else>Loading...</div>
13
</template>
14
<script>
15
import { FormKitSchema } from "@formkit/vue"
16
import { __ } from "../../i18n"
17
18
export default {
19
    name: "Form",
20
    data() {
21
        return {
22
            schemaData: {
23
                doCancel: () => {
24
                    this.$emit("cancel")
25
                },
26
            },
27
            formSchema: [
28
                {
29
                    $el: "fieldset",
30
                    attrs: {
31
                        class: "rows",
32
                    },
33
                    children: this.schema,
34
                },
35
                {
36
                    $el: "fieldset",
37
                    attrs: {
38
                        class: "action",
39
                    },
40
                    children: [
41
                        {
42
                            $el: "input",
43
                            attrs: {
44
                                type: "submit",
45
                                class: "btn btn-primary",
46
                            },
47
                            ignore: false,
48
                            children: this.submitMessage,
49
                        },
50
                        {
51
                            $el: "a",
52
                            attrs: {
53
                                class: "cancel",
54
                                onClick: "$doCancel",
55
                            },
56
                            children: this.cancelMessage,
57
                        },
58
                    ],
59
                },
60
            ],
61
        }
62
    },
63
    methods: {
64
        async doSubmit(event) {
65
            this.$emit("submit", event)
66
        },
67
    },
68
    components: {
69
        FormKitSchema,
70
    },
71
    emits: ["submit", "cancel"],
72
    props: {
73
        schema: Object,
74
        loading: {
75
            type: Boolean,
76
            default: false,
77
        },
78
        data: {
79
            type: Object,
80
            default: {},
81
        },
82
        submitMessage: {
83
            type: String,
84
            default: __("Submit"),
85
        },
86
        cancelMessage: {
87
            type: String,
88
            default: __("Cancel"),
89
        },
90
    },
91
}
92
</script>
93
94
<style>
95
.formkit-value {
96
    align-items: center;
97
    background-color: #f5f5f5;
98
    border-radius: 0.25em;
99
    box-sizing: border-box;
100
    color: black;
101
    display: flex;
102
    justify-content: space-between;
103
    padding: 0.55em 0.5em;
104
    text-decoration: none;
105
    width: 100%;
106
}
107
108
a.formkit-value:hover {
109
    text-decoration: none;
110
}
111
112
.formkit-value::after {
113
    content: "\00D7";
114
    margin-left: 0.5em;
115
    font-size: 1.1em;
116
}
117
118
.formkit-dropdown {
119
    position: absolute;
120
    top: 100%;
121
    left: 0;
122
    min-width: 15em;
123
    background-color: white;
124
    box-shadow: 0 0 0.5em rgb(0 0 0 / 10%);
125
    margin: 0;
126
    padding: 0;
127
    list-style-type: none;
128
    overflow: hidden;
129
    border-radius: 0.25em;
130
}
131
132
.formkit-dropdown-item[data-selected="true"] {
133
    background-color: #cfe8fc;
134
}
135
136
.formkit-dropdown-item {
137
    padding: 0.5em;
138
    border-bottom: 1px solid #e4e4e4;
139
}
140
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/shared/KohaTable.vue (+433 lines)
Line 0 Link Here
1
<template>
2
    <DataTable
3
        :columns="tableColumns"
4
        :options="{ ...dataTablesDefaults, ...options, ...mandatoryOptions }"
5
        :data="data"
6
        ref="table"
7
    >
8
        <slot></slot>
9
    </DataTable>
10
</template>
11
12
<script>
13
import DataTable from "datatables.net-vue3"
14
import DataTablesLib from "datatables.net"
15
import { inject, nextTick } from "vue"
16
17
DataTable.use(DataTablesLib)
18
19
function toggledClearFilter(searchText, tableId) {
20
    if (searchText == "") {
21
        $("#" + tableId + "_wrapper")
22
            .find(".dt_button_clear_filter")
23
            .addClass("disabled")
24
    } else {
25
        $("#" + tableId + "_wrapper")
26
            .find(".dt_button_clear_filter")
27
            .removeClass("disabled")
28
    }
29
}
30
31
export default {
32
    name: "KohaTable",
33
    data() {
34
        return {
35
            errorMessage: "",
36
            tableColumns: this.columns,
37
            mandatoryOptions: {
38
                deferRender: true,
39
                paging: true,
40
                serverSide: true,
41
                searching: true,
42
                pagingType: "full_numbers",
43
                processing: true,
44
                ajax: async (data, callback, settings) => {
45
                    const options = {
46
                        ...this.dataTablesDefaults,
47
                        ...this.options,
48
                        ...this.mandatoryOptions,
49
                    }
50
                    try {
51
                        const headers = {}
52
                        if (options.embed) {
53
                            headers["x-koha-embed"] = Array.isArray(
54
                                options.embed
55
                            )
56
                                ? options.embed.join(",")
57
                                : options.embed
58
                        }
59
60
                        const length = data.length
61
                        const start = data.start
62
63
                        const dataSet = {
64
                            _page: Math.floor(start / length) + 1,
65
                            _per_page: length,
66
                        }
67
68
                        const build_query = (col, value) => {
69
                            const parts = []
70
                            const attributes = col.data.split(":")
71
                            return attributes.map(attr => {
72
                                let criteria = options.criteria
73
                                if (value.match(/^\^(.*)\$$/)) {
74
                                    value = value
75
                                        .replace(/^\^/, "")
76
                                        .replace(/\$$/, "")
77
                                    criteria = "exact"
78
                                } else {
79
                                    // escape SQL LIKE special characters % and _
80
                                    value = value.replace(/(\%|\\)/g, "\\$1")
81
                                }
82
                                return {
83
                                    [!attr.includes(".") ? "me." + attr : attr]:
84
                                        criteria === "exact"
85
                                            ? value
86
                                            : {
87
                                                  like:
88
                                                      ([
89
                                                          "contains",
90
                                                          "ends_with",
91
                                                      ].indexOf(criteria) !== -1
92
                                                          ? "%"
93
                                                          : "") +
94
                                                      value +
95
                                                      ([
96
                                                          "contains",
97
                                                          "starts_with",
98
                                                      ].indexOf(criteria) !== -1
99
                                                          ? "%"
100
                                                          : ""),
101
                                              },
102
                                }
103
                            })
104
                        }
105
106
                        const filter = data.search.value
107
                        // Build query for each column filter
108
                        const and_query_parameters = settings.aoColumns
109
                            .filter(function (col) {
110
                                return (
111
                                    col.bSearchable &&
112
                                    typeof col.data == "string" &&
113
                                    data.columns[col.idx].search.value != ""
114
                                )
115
                            })
116
                            .map(function (col) {
117
                                const value = data.columns[col.idx].search.value
118
                                return build_query(col, value)
119
                            })
120
                            .map(function r(e) {
121
                                return $.isArray(e) ? $.map(e, r) : e
122
                            })
123
124
                        // Build query for the global search filter
125
                        const or_query_parameters = settings.aoColumns
126
                            .filter(function (col) {
127
                                return col.bSearchable && filter != ""
128
                            })
129
                            .map(function (col) {
130
                                const value = filter
131
                                return build_query(col, value)
132
                            })
133
                            .map(function r(e) {
134
                                return $.isArray(e) ? $.map(e, r) : e
135
                            })
136
137
                        if (this.default_filters) {
138
                            const additional_filters = {}
139
                            for (f in this.default_filters) {
140
                                let k
141
                                let v
142
                                if (
143
                                    typeof this.default_filters[f] ===
144
                                    "function"
145
                                ) {
146
                                    const val = this.default_filters[f]()
147
                                    if (val != undefined && val != "") {
148
                                        k = f
149
                                        v = val
150
                                    }
151
                                } else {
152
                                    k = f
153
                                    v = this.default_filters[f]
154
                                }
155
156
                                // Pass to -or if you want a separate OR clause
157
                                // It's not the usual DBIC notation!
158
                                if (f == "-or") {
159
                                    if (v) or_query_parameters.push(v)
160
                                } else if (f == "-and") {
161
                                    if (v) and_query_parameters.push(v)
162
                                } else if (v) {
163
                                    additional_filters[k] = v
164
                                }
165
                            }
166
                            if (Object.keys(additional_filters).length) {
167
                                and_query_parameters.push(additional_filters)
168
                            }
169
                        }
170
                        let query_parameters = and_query_parameters
171
                        if (or_query_parameters.length) {
172
                            query_parameters.push(or_query_parameters)
173
                        }
174
175
                        if (query_parameters.length) {
176
                            query_parameters = JSON.stringify(
177
                                query_parameters.length === 1
178
                                    ? query_parameters[0]
179
                                    : { "-and": query_parameters }
180
                            )
181
                            if (options.header_filter) {
182
                                headers["x-koha-query"] = query_parameters
183
                            } else {
184
                                dataSet.q = query_parameters
185
                            }
186
                        }
187
188
                        dataSet._match = options.criteria
189
190
                        if (data.draw !== undefined) {
191
                            headers["x-koha-request-id"] = data.draw
192
                        }
193
194
                        if (options.columns) {
195
                            const order = data.order
196
                            const orderArray = new Array()
197
                            order.forEach(function (e, i) {
198
                                const order_col = e.column
199
                                let order_by = options.columns[order_col].data
200
                                order_by = order_by.split(":")
201
                                const order_dir = e.dir == "asc" ? "+" : "-"
202
                                Array.prototype.push.apply(
203
                                    orderArray,
204
                                    order_by.map(
205
                                        x =>
206
                                            order_dir +
207
                                            (!x.includes(".") ? "me." + x : x)
208
                                    )
209
                                )
210
                            })
211
                            dataSet._order_by = orderArray
212
                                .filter((v, i, a) => a.indexOf(v) === i)
213
                                .join(",")
214
                        }
215
216
                        const query = new URLSearchParams(dataSet)
217
218
                        const response = await this.list({ query, headers })
219
                        if (!response.ok) {
220
                            throw Error(
221
                                `${response.status} ${response.statusText}`
222
                            )
223
                        }
224
                        const json = { data: response.data }
225
                        if (response.headers["x-total-count"]) {
226
                            const total = response.headers["x-total-count"]
227
                            json.recordsTotal = total
228
                            json.recordsFiltered = total
229
                        }
230
                        if (response.headers["x-base-total-count"]) {
231
                            json.recordsTotal =
232
                                response.headers["x-base-total-count"]
233
                        }
234
                        if (response.headers["x-koha-request-id"]) {
235
                            json.draw = response.headers["x-koha-request-id"]
236
                        }
237
                        callback(json)
238
                    } catch (error) {
239
                        this.errorMessage = `<div>${this.$__(
240
                            "Error retrieving data: %s"
241
                        ).format(error.message)}</div>`
242
                        callback([])
243
                    }
244
                },
245
                serverSide: true,
246
            },
247
            dataTablesDefaults: {
248
                language: {
249
                    paginate: {
250
                        first: this.$__("First"),
251
                        last: this.$__("Last"),
252
                        next: this.$__("Next"),
253
                        previous: this.$__("Previous"),
254
                    },
255
                    emptyTable: this.options.emptyTable
256
                        ? this.options.emptyTable
257
                        : this.$__("No data available in table"),
258
                    info: this.$__(
259
                        "Showing _START_ to _END_ of _TOTAL_ entries"
260
                    ),
261
                    infoEmpty: this.$__("No entries to show"),
262
                    infoFiltered: this.$__(
263
                        "(filtered from _MAX_ total entries)"
264
                    ),
265
                    lengthMenu: this.$__("Show _MENU_ entries"),
266
                    loadingRecords: this.$__("Loading..."),
267
                    processing: this.$__("Processing..."),
268
                    search: this.$__("Search:"),
269
                    zeroRecords: this.$__("No matching records found"),
270
                    buttons: {
271
                        copyTitle: this.$__("Copy to clipboard"),
272
                        copyKeys: this.$__(
273
                            "Press <i>ctrl</i> or <i>⌘</i> + <i>C</i> to copy the table data<br>to your system clipboard.<br><br>To cancel, click this message or press escape."
274
                        ),
275
                        copySuccess: {
276
                            _: this.$__("Copied %d rows to clipboard"),
277
                            1: this.$__("Copied one row to clipboard"),
278
                        },
279
                    },
280
                },
281
                dom: '<"dt-info"i><"top pager"<"table_entries"lp><"table_controls"fB>>tr<"bottom pager"ip>',
282
                buttons: [
283
                    {
284
                        fade: 100,
285
                        className: "dt_button_clear_filter",
286
                        titleAttr: this.$__("Clear filter"),
287
                        enabled: false,
288
                        text:
289
                            '<i class="fa fa-lg fa-remove"></i> <span class="dt-button-text">' +
290
                            this.$__("Clear filter") +
291
                            "</span>",
292
                        available: function (dt) {
293
                            // The "clear filter" button is made available if this test returns true
294
                            if (dt.settings()[0].aanFeatures.f) {
295
                                // aanFeatures.f is null if there is no search form
296
                                return true
297
                            }
298
                        },
299
                        action: function (e, dt, node) {
300
                            dt.search("").draw("page")
301
                            node.addClass("disabled")
302
                        },
303
                    },
304
                ],
305
                lengthMenu: [
306
                    [10, 20, 50, 100, -1],
307
                    [10, 20, 50, 100, this.$__("All")],
308
                ],
309
                pageLength: 20,
310
                fixedHeader: true,
311
                criteria: "contains",
312
                initComplete: function (settings) {
313
                    var tableId = settings.nTable.id
314
                    var state = settings.oLoadedState
315
                    state && toggledClearFilter(state.search.search, tableId)
316
                    // When the DataTables search function is triggered,
317
                    // enable or disable the "Clear filter" button based on
318
                    // the presence of a search string
319
                    $("#" + tableId).on("search.dt", function (e, settings) {
320
                        toggledClearFilter(
321
                            settings.oPreviousSearch.sSearch,
322
                            tableId
323
                        )
324
                    })
325
                },
326
            },
327
        }
328
    },
329
    setup() {
330
        const { data, list, setOptions } = inject("tableStore")
331
        const { setError, removeMessages, setWarning } = inject("mainStore")
332
        return { data, list, setOptions, setError, removeMessages, setWarning }
333
    },
334
    beforeMount() {
335
        if (this.url) this.setOptions(this.url)
336
        if (this.edit || this.remove) {
337
            this.tableColumns = [
338
                ...this.tableColumns,
339
                {
340
                    name: "actions",
341
                    title: this.$__("Actions"),
342
                    searchable: false,
343
                    render: (data, type, row) => {
344
                        let content = ""
345
                        if (this.edit) {
346
                            content +=
347
                                '<button class="edit btn"><i class="fa fa-edit"></i></button>'
348
                        }
349
                        if (this.remove) {
350
                            content +=
351
                                '<button class="remove btn"><i class="fa fa-trash"></i></button>'
352
                        }
353
                        return content
354
                    },
355
                },
356
            ]
357
        }
358
        // this.dt = this.$refs.table.value.dt();
359
    },
360
    mounted() {
361
        if (this.edit || this.remove) {
362
            const dt = this.$refs.table.dt()
363
            const self = this
364
            dt.on("draw", () => {
365
                const dataSet = dt.rows().data()
366
                dt.column(-1)
367
                    .nodes()
368
                    .to$()
369
                    .each(function (idx) {
370
                        const data = dataSet[idx]
371
                        console.log(this)
372
                        $(".edit", this).on("click", () => {
373
                            self.$emit("edit", data)
374
                        })
375
                        $(".remove", this).on("click", () => {
376
                            self.setWarning(
377
                                `<h3>${self.$__(
378
                                    "Remove element"
379
                                )}</h3><p>${self.$__(
380
                                    "Are you sure you want to remove this element?"
381
                                )}</p>`,
382
                                () => {
383
                                    self.$emit("remove", data, () => {
384
                                        dt.draw()
385
                                    })
386
                                }
387
                            )
388
                        })
389
                    })
390
            })
391
        }
392
    },
393
    beforeUnmount() {
394
        const dt = this.$refs.table.dt()
395
        dt.destroy()
396
    },
397
    watch: {
398
        errorMessage(newError) {
399
            this.removeMessages()
400
            if (newError) this.setError(newError)
401
        },
402
    },
403
    components: {
404
        DataTable,
405
    },
406
    props: {
407
        url: {
408
            type: String,
409
            default: "",
410
        },
411
        columns: {
412
            type: Array,
413
            default: [],
414
        },
415
        edit: {
416
            type: Boolean,
417
            default: true,
418
        },
419
        remove: {
420
            type: Boolean,
421
            default: true,
422
        },
423
        options: {
424
            type: Object,
425
            default: {},
426
        },
427
        default_filters: {
428
            type: Object,
429
            required: false,
430
        },
431
    },
432
}
433
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/shared/Menu.vue (+57 lines)
Line 0 Link Here
1
<template>
2
    <aside>
3
        <div id="navmenu">
4
            <div id="navmenulist">
5
                <h5>{{ $__(title) }}</h5>
6
                <ul>
7
                    <MenuItem
8
                        v-for="(item, key) in menu"
9
                        v-bind:key="key"
10
                        :item="item"
11
                    ></MenuItem>
12
                </ul>
13
            </div>
14
        </div>
15
    </aside>
16
</template>
17
18
<script>
19
import { inject } from "vue"
20
import MenuItem from "./MenuItem.vue"
21
export default {
22
    name: "Menu",
23
    setup: () => {
24
        const menuStore = inject("menuStore")
25
        const { menu } = menuStore
26
        return {
27
            menu,
28
        }
29
    },
30
    props: {
31
        title: String,
32
    },
33
    components: {
34
        MenuItem,
35
    },
36
}
37
</script>
38
39
<style scoped>
40
#navmenulist a.router-link-active {
41
    font-weight: 700;
42
}
43
#menu ul ul,
44
#navmenulist ul ul {
45
    padding-left: 2em;
46
    font-size: 100%;
47
}
48
49
#navmenulist ul li a.disabled {
50
    color: #666;
51
    pointer-events: none;
52
    font-weight: 700;
53
}
54
#navmenulist ul li a.disabled.router-link-active {
55
    color: #000;
56
}
57
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/shared/MenuItem.vue (+41 lines)
Line 0 Link Here
1
<template>
2
    <li>
3
        <span>
4
            <router-link v-if="item.path" :to="item.path">
5
                <i v-if="item.icon" :class="`fa ${item.icon}`"></i>
6
                <span v-if="item.title">{{ $__(item.title) }}</span>
7
            </router-link>
8
            <a v-else-if="item.href" :href="item.href">
9
                <i v-if="item.icon" :class="`fa ${item.icon}`"></i>
10
                <span v-if="item.title">{{ $__(item.title) }}</span>
11
            </a>
12
            <span v-else>
13
                <i v-if="item.icon" :class="`fa ${item.icon}`"></i>
14
                <span class="item-last" v-if="item.title">{{
15
                    $__(item.title)
16
                }}</span>
17
            </span>
18
        </span>
19
        <ul v-if="item.children && item.children.length">
20
            <MenuItem
21
                v-for="(item, key) in item.children"
22
                :item="item"
23
            ></MenuItem>
24
        </ul>
25
    </li>
26
</template>
27
28
<script>
29
export default {
30
    name: "MenuItem",
31
    props: {
32
        item: Object,
33
    },
34
}
35
</script>
36
37
<style>
38
span.item-last {
39
    padding: 7px 3px;
40
}
41
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/shared/Page.vue (+48 lines)
Line 0 Link Here
1
<template>
2
    <div :id="id">
3
        <Breadcrumbs :title="title"></Breadcrumbs>
4
        <div class="main container-fluid">
5
            <div class="row">
6
                <template v-if="sideMenu">
7
                    <div class="col-sm-10 col-sm-push-2">
8
                        <Dialog></Dialog>
9
                        <slot></slot>
10
                    </div>
11
                    <div class="col-sm-2 col-sm-pull-10">
12
                        <Menu :title="title"></Menu>
13
                    </div>
14
                </template>
15
                <template v-else>
16
                    <div class="col-sm-12">
17
                        <Dialog></Dialog>
18
                        <slot></slot>
19
                    </div>
20
                </template>
21
            </div>
22
        </div>
23
    </div>
24
</template>
25
26
<script>
27
import Menu from "./Menu.vue"
28
import Breadcrumbs from "./Breadcrumbs.vue"
29
import Dialog from "./Dialog.vue"
30
export default {
31
    name: "Page",
32
    data: () => ({
33
        sideMenu: true,
34
    }),
35
    components: {
36
        Menu,
37
        Dialog,
38
        Breadcrumbs,
39
    },
40
    props: {
41
        id: String,
42
        title: String,
43
        sideMenu: Boolean,
44
    },
45
}
46
</script>
47
48
<style></style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js (+9 lines)
Line 0 Link Here
1
export const __ = (key) => {
2
  return window["__"](key);
3
};
4
5
export const i18n = {
6
  install: (app, options) => {
7
      app.config.globalProperties.$__ = __
8
  },
9
};
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/main.ts (+77 lines)
Line 0 Link Here
1
import { createApp } from "vue";
2
import { createPinia } from "pinia";
3
import { plugin as formkit, defaultConfig } from '@formkit/vue'
4
import '@formkit/themes/genesis'
5
import getRouter from './router';
6
7
import { library } from "@fortawesome/fontawesome-svg-core";
8
import {
9
    faPlus,
10
    faMinus,
11
    faPencil,
12
    faTrash,
13
    faSpinner,
14
} from "@fortawesome/free-solid-svg-icons";
15
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
16
import vSelect from "vue-select";
17
import { useMenuStore } from '../stores/menu';
18
import { useMainStore } from '../stores/main';
19
import { useTableStore } from '../stores/table';
20
import { useModelStore } from '../stores/model';
21
import tree from './tree';
22
import model from './model';
23
24
library.add(faPlus, faMinus, faPencil, faTrash, faSpinner);
25
26
const pinia = createPinia();
27
const menuStore = useMenuStore(pinia);
28
const mainStore = useMainStore(pinia);
29
const tableStore = useTableStore(pinia);
30
const modelStore = useModelStore(pinia);
31
const { removeMessages } = mainStore;
32
const { setTree } = menuStore;
33
const { setModel } = modelStore;
34
const { setOptions } = tableStore;
35
setTree(tree);
36
setModel(model);
37
setOptions('/api/v1/import_sources')
38
39
const router = getRouter(menuStore);
40
41
import App from "../components/import-sources/ISMain.vue";
42
43
// import { routes } from "./routes";
44
45
// const router = createRouter({
46
//     history: createWebHistory(),
47
//     linkExactActiveClass: "current",
48
//     routes,
49
// });
50
51
// import { useAVStore } from "../stores/authorised_values";
52
53
import { i18n } from "../i18n"
54
import inputs from "../inputs"
55
const app = createApp(App);
56
57
const rootComponent = app
58
    .use(i18n)
59
    .use(pinia)
60
    .use(router)
61
    .use(formkit, defaultConfig({
62
        inputs
63
    }))
64
    .component("font-awesome-icon", FontAwesomeIcon)
65
    .component("v-select", vSelect);
66
67
app.config.unwrapInjectedRef = true;
68
app.provide("mainStore", mainStore);
69
app.provide("menuStore", menuStore);
70
app.provide("tableStore", tableStore);
71
app.provide("modelStore", modelStore);
72
app.mount("#import-source");
73
74
router.beforeEach((to, from) => {
75
    menuStore.$patch({current: to.meta.self});
76
    removeMessages(); // This will actually flag the messages as displayed already
77
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/model.js (+83 lines)
Line 0 Link Here
1
import { __ } from "../i18n";
2
3
export default [
4
  {
5
    name: 'import_source_id',
6
    table: {
7
      data: 'import_source_id',
8
      visible: false
9
    },
10
    form: {
11
      type: 'hidden'
12
    }
13
  },
14
  {
15
    name: 'name',
16
    title: __('Source name'),
17
    table: {
18
      data: 'name',
19
      searchable: true
20
    },
21
    form: {
22
      type: 'text',
23
      label: {
24
        $ref: 'title'
25
      },
26
      validation: 'required'
27
    }
28
  },
29
  {
30
    name: 'patron_id',
31
    title: __('Patron'),
32
    table: {
33
      searchable: true,
34
      data: "patron.firstname:patron.surname:patron.middle_name",
35
      render: (data, type, row) => {
36
        const {firstname, surname, middle_name} = row.patron
37
        return `${firstname?firstname:''} ${middle_name?`${middle_name} `:''}${surname?surname:''}`.trim()
38
      }
39
    },
40
    form: {
41
      type: 'autocomplete',
42
      label: 'Choose a patron',
43
      validation: 'required',
44
      optionLoader: async (id) => {
45
        const response = await fetch(`/api/v1/patrons/${id}`)
46
        if(!response.ok) return null
47
        const {firstname, surname, middle_name, patron_id} = await response.json()
48
        return {value: patron_id, label: `${firstname?firstname:''} ${middle_name?`${middle_name} `:''}${surname?surname:''}`.trim()}
49
      },
50
      options: async (value) => {
51
        let query = '';
52
        if(value !== undefined) {
53
          query = new URLSearchParams({
54
            q: JSON.stringify([
55
              {
56
                firstname: {
57
                  like: `%${value}%`
58
                }
59
              },
60
              {
61
                middle_name: {
62
                  like: `%${value}%`
63
                }
64
              },
65
              {
66
                surname: {
67
                  like: `%${value}%`
68
                }
69
              }
70
            ])
71
          })
72
        }
73
        const response = await fetch(`/api/v1/patrons${query?`?${query}`:''}`)
74
        if(!response.ok) return []
75
        const rows = await response.json()
76
        return rows.map(row => {
77
          const {firstname, surname, middle_name, patron_id} = row
78
          return {value: patron_id, label: `${firstname?firstname:''} ${middle_name?`${middle_name} `:''}${surname?surname:''}`.trim()}
79
        })
80
      }
81
    }
82
  }
83
]
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js (+12 lines)
Line 0 Link Here
1
import { createWebHistory, createRouter } from "vue-router";
2
3
export default (menuStore) => {
4
5
  const { routes } = menuStore
6
  const router = createRouter({
7
      history: createWebHistory(),
8
      linkExactActiveClass: "current",
9
      routes,
10
  })
11
  return router
12
}
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/tree.js (+31 lines)
Line 0 Link Here
1
import ISEdit from '../components/import-sources/ISEdit.vue'
2
import ISList from '../components/import-sources/ISList.vue'
3
4
export default {
5
  title: 'Administration',
6
  href: '/cgi-bin/koha/admin/admin-home.pl',
7
  children: [
8
    {
9
      title: 'Import sources',
10
      path: '/cgi-bin/koha/admin/import-sources.pl',
11
      component: {template: '<router-view></router-view>', name: 'ISBase'},
12
      children: [
13
        {
14
          title: 'List',
15
          path: '',
16
          component: ISList
17
        },
18
        {
19
          title: 'Add import source',
20
          path: 'add',
21
          component: ISEdit
22
        },
23
        {
24
          title: 'Edit import source',
25
          path: ':id',
26
          component: ISEdit
27
        }
28
      ]
29
    }
30
  ]
31
}
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js (+199 lines)
Line 0 Link Here
1
import { createInput } from '@formkit/vue'
2
3
/**
4
 * This is an input "feature" — a function that accepts a node and exposes
5
 * some additional functionality to an input. When using schemas, this can
6
 * take the place of a traditional "script" block in a Vue component. In this
7
 * example, we expose:
8
 *
9
 *   - An input handler `search`.
10
 *   - An input handler `selections`.
11
 *   - Commit middleware to place filtered options into the `matches` prop.
12
 *
13
 * Once written, input features are added via the input declaration.
14
 */
15
const searchFeature = (node) => {
16
17
  const _transform_options = (options) => {
18
    if(!Array.isArray(options)) {
19
      return Object.keys(options).map((key) => ({label: options[key], value: key}))
20
    }
21
    return options.map((option) => {
22
      if(typeof option === 'string') return {label: option, value: option}
23
      return option
24
    })
25
  }
26
27
  // We wait for our node to be fully  "created" before we start to add our
28
  // handlers to ensure the core Vue plugin has added its context object:
29
  node.on('created', async () => {
30
    // Ensure our matches prop starts as an array.
31
    node.props.matches = []
32
    node.props.selection = ''
33
34
    if(node.value !== undefined && node.props.attrs && typeof node.props.attrs.optionLoader === 'function') {
35
      node.props.selection = await node.props.attrs.optionLoader(node.value)
36
    }
37
38
    const clearValue = async (e) => {
39
      if (e && typeof e.preventDefault === 'function') e.preventDefault()
40
      node.input('')
41
      node.props.matches = []
42
      node.props.selection = ''
43
      node.props.searchValue = ''
44
      setTimeout(() => {
45
        if (document.querySelector('input#' + node.props.id)) {
46
          document.querySelector('input#' + node.props.id).focus()
47
        }
48
      }, 50)
49
    }
50
    // When we actually have an value to set:
51
    const setValue = async (e) => {
52
      if (e && typeof e.preventDefault === 'function') e.preventDefault()
53
      node.input(node.props.selection.value)
54
      node.props.display = node.props.selection.label
55
      node.props.searchValue = ''
56
      await new Promise((r) => setTimeout(r, 50)) // "next tick"
57
    }
58
59
    // Perform a soft selection, this is shown as a highlight in the dropdown
60
    const select = (delta) => {
61
      const available = node.props.matches
62
      let idx = available.indexOf(node.props.selection) + delta
63
      if (idx >= available.length) {
64
        idx = 0
65
      } else if (idx < 0) {
66
        idx = available.length - 1
67
      }
68
      node.props.selection = available[idx]
69
    }
70
71
    // Add some new "handlers" for our autocomplete. The handlers object is
72
    // just a conventionally good place to put event handlers. Auto complete
73
    // inputs always have to deal with lots of keyboard events, so that logic
74
    // is registered here.
75
    Object.assign(node.context.handlers, {
76
      setValue,
77
      clearValue,
78
      selection: (e) => {
79
        // This handler is called when entering data into the search input.
80
        switch (e.key) {
81
          case 'Enter':
82
            return setValue()
83
          case 'ArrowDown':
84
            e.preventDefault()
85
            return select(1)
86
          case 'ArrowUp':
87
            e.preventDefault()
88
            return select(-1)
89
          case 'Escape':
90
            return node.props.matches = []
91
        }
92
      },
93
      blur: (e) => {
94
        if(!node.props.hovering) node.props.matches = []
95
      },
96
      search(e) {
97
        node.props.searchValue = e.target.value
98
      },
99
      hover: (e) => {
100
        node.props.hovering = true
101
        node.props.selection = node.props.matches.find(match => String(match.value) === e.target.attributes.value.value)
102
        console.log(node.props.selection)
103
      },
104
      unhover: (e) => {
105
        if (node.props.matches.find(match => String(match.value) === e.target.attributes.value.value) === node.props.selection) {
106
          node.props.selection = ''
107
          node.props.hovering = false
108
        }
109
      },
110
    })
111
  })
112
113
  // Perform filtering when the search value changes
114
  node.on('prop:searchValue', async ({ payload: value }) => {
115
    let results
116
    if(typeof node.props.options === 'function') {
117
      results = _transform_options(await node.props.options(value))
118
    } else {
119
      results = _transform_options(node.props.options).filter((option) =>
120
        option.label.toLowerCase().split(/\W/).find((part).startsWith(value.toLowerCase()))
121
      )
122
    }
123
    //if (!results.length) results.push('No matches')
124
    node.props.matches = results
125
  })
126
}
127
128
/**
129
 * This is our input schema responsible for rendering the inner “input”
130
 * section. In our example, we render an text input which will be used
131
 * to filter search results, and an unordered list that shows all remaining
132
 * matches.
133
 */
134
const schema = {
135
  if: '$value',
136
  then: [
137
    {
138
      $el: 'a',
139
      attrs: {
140
        id: '$id',
141
        href: '#',
142
        class: '$classes.value',
143
        onClick: '$handlers.clearValue',
144
      },
145
      children: '$selection.label'
146
    }
147
  ],
148
  else: [
149
    {
150
      $el: 'input',
151
      bind: '$attrs',
152
      attrs: {
153
        id: '$id',
154
        class: '$classes.input',
155
        onKeydown: '$handlers.selection',
156
        onInput: '$handlers.search',
157
        onBlur: '$handlers.blur',
158
        value: '$searchValue',
159
      },
160
    },
161
    {
162
      $el: 'ul',
163
      if: '$matches.length',
164
      attrs: {
165
        class: '$classes.dropdown',
166
      },
167
      children: [
168
        {
169
          $el: 'li',
170
          for: ['match', '$matches'],
171
          attrs: {
172
            'data-selected': {
173
              if: '$selection === $match',
174
              then: 'true',
175
              else: 'false',
176
            },
177
            value: '$match.value',
178
            class: '$classes.dropdownItem',
179
            onClick: '$handlers.setValue',
180
            onMouseenter: '$handlers.hover',
181
            onMouseleave: '$handlers.unhover',
182
          },
183
          children: '$match.label',
184
        },
185
      ],
186
    },
187
  ],
188
}
189
190
/**
191
 * Finally we create our actual input declaration by using `createInput` this
192
 * places our schema into a "standard" FormKit schema feature set with slots,
193
 * labels, help, messages etc. The return value of this function is a proper
194
 * input declaration.
195
 */
196
export default createInput(schema, {
197
  props: ['options', 'matches', 'selection', 'searchValue', 'value'],
198
  features: [searchFeature],
199
})
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js (+5 lines)
Line 0 Link Here
1
import autocomplete from './autocomplete'
2
3
export default {
4
  autocomplete
5
}
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/main.js (-19 / +41 lines)
Lines 2-38 import { defineStore } from "pinia"; Link Here
2
2
3
export const useMainStore = defineStore("main", {
3
export const useMainStore = defineStore("main", {
4
    state: () => ({
4
    state: () => ({
5
        message: null,
5
        _message: null,
6
        error: null,
6
        _error: null,
7
        warning: null,
7
        _warning: null,
8
        _accept: null,
8
        previousMessage: null,
9
        previousMessage: null,
9
        previousError: null,
10
        previousError: null,
10
        displayed_already: false,
11
        displayed_already: false,
11
    }),
12
    }),
12
    actions: {
13
    actions: {
13
        setMessage(message) {
14
        setMessage(message, displayed = false) {
14
            this.error = null;
15
            this._error = null;
15
            this.warning = null;
16
            this._warning = null;
16
            this.message = message;
17
            this._message = message;
17
            this.displayed_already = false; /* Will be displayed on the next view */
18
            this.displayed_already = displayed; /* Will be displayed on the next view */
18
        },
19
        },
19
        setError(error) {
20
        setError(error, displayed = true) {
20
            this.error = error;
21
            this._error = error;
21
            this.message = null;
22
            this._message = null;
22
            this.displayed_already = true; /* Is displayed on the current view */
23
            this.displayed_already = displayed; /* Is displayed on the current view */
23
        },
24
        },
24
        setWarning(warning) {
25
        setWarning(warning, accept, displayed = true) {
25
            this.warning = warning;
26
            if(accept) {
26
            this.message = null;
27
                this._accept = async () => {
27
            this.displayed_already = true; /* Is displayed on the current view */
28
                    await accept()
29
                    this.removeMessages()
30
                }
31
            }
32
            this._warning = warning;
33
            this._message = null;
34
            this.displayed_already = displayed; /* Is displayed on the current view */
28
        },
35
        },
29
        removeMessages() {
36
        removeMessages() {
30
            if (this.displayed_already) {
37
            if (this.displayed_already) {
31
                this.error = null;
38
                this._accept = null
32
                this.warning = null;
39
                this._error = null;
33
                this.message = null;
40
                this._warning = null;
41
                this._message = null;
34
            }
42
            }
35
            this.displayed_already = true;
43
            this.displayed_already = true;
36
        },
44
        },
37
    },
45
    },
46
    getters: {
47
        error() {
48
            return this._error
49
        },
50
        warning() {
51
            return this._warning
52
        },
53
        message() {
54
            return this._message
55
        },
56
        accept() {
57
            return this._accept
58
        }
59
    }
38
});
60
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/menu.js (+100 lines)
Line 0 Link Here
1
import { defineStore } from "pinia";
2
3
export const useMenuStore = defineStore("menu", {
4
  state: () => ({
5
    tree: {
6
      alt: "Home",
7
      href: "/cgi-bin/koha/mainpage.pl",
8
      is_menu_item: false,
9
      is_base: true,
10
      children: []
11
    },
12
    current: null,
13
  }),
14
  actions: {
15
    setTree(treeDef) {
16
      const _traverse_children = (parent) => {
17
        if(parent.children && parent.children.length) {
18
          parent.children.forEach(child => {
19
            child.parent = parent;
20
            if(child.path) {
21
              child.meta = {
22
                self: child
23
              }
24
            }
25
            if(parent.children.length === 1 && parent.is_base) {
26
              child.is_base = true;
27
              child.is_menu_item = false;
28
            } else if (parent.children.length > 1 || !parent.is_base) {
29
              child.is_base = false;
30
            }
31
            _traverse_children(child)
32
          })
33
        }
34
      }
35
      if(!Array.isArray(treeDef)) {
36
        treeDef = [treeDef]
37
      }
38
      this.tree.children = treeDef
39
      _traverse_children(this.tree)
40
    }
41
  },
42
  getters: {
43
    breadcrumbs() {
44
      const _traverse_parent = (child) => {
45
        let breadcrumbs = []
46
        let builtPath = child.path
47
        if(child.parent) {
48
          breadcrumbs = _traverse_parent(child.parent)
49
          if(builtPath !== undefined && !/^\//.test(builtPath)) {
50
            const {path:parentPath} = breadcrumbs[breadcrumbs.length - 1]
51
            if(parentPath !== undefined) {
52
              builtPath = `${parentPath}${/\/$/.test(parentPath)?'':'/'}${builtPath}`
53
            }
54
          }
55
        }
56
        breadcrumbs.push({...child, path: builtPath, children: null})
57
        return breadcrumbs
58
      }
59
      if(this.current)
60
        return _traverse_parent(this.current)
61
62
      const _get_base_elements = (parent) => {
63
        if(!parent.is_base) return []
64
        return [{...parent, children: null}, ..._get_base_elements(parent.children[0])]
65
      }
66
67
      return _get_base_elements(this.tree)
68
    },
69
    menu() {
70
      const _get_menu_elements = (parent, builtPath = '') => {
71
        if (parent.is_base && parent.children.length === 1) return _get_menu_elements(parent.children[0])
72
        let items = []
73
        if(builtPath) builtPath = `${builtPath}${/\/$/.test(builtPath)?'':'/'}`
74
        if(parent.path !== undefined && /^\//.test(parent.path)) {
75
          builtPath = parent.path
76
        } else if (parent.path  !== undefined) {
77
          builtPath = `${builtPath}${parent.path}`
78
        } else {
79
          builtPath = ''
80
        }
81
        if (parent.children && parent.children.length) {
82
          items = parent.children
83
            .filter(child => child.is_menu_item)
84
            .map(child => _get_menu_elements(child, builtPath))
85
        }
86
        if(!parent.is_menu_item) return items
87
        return {...parent, path: builtPath ? builtPath : parent.path, children: items}
88
      }
89
      return _get_menu_elements(this.tree)
90
    },
91
    routes() {
92
      const _to_route = (parent) => {
93
        if(parent.path === undefined) return parent.children.map(child => _to_route(child)).flat(Infinity)
94
        return parent
95
      }
96
      let routes = _to_route(this.tree)
97
      return Array.isArray(routes) ? routes : [routes]
98
    }
99
  }
100
})
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js (+51 lines)
Line 0 Link Here
1
import { defineStore } from "pinia";
2
3
export const useModelStore = defineStore("model", {
4
    state: () => ({
5
        model: []
6
    }),
7
    actions: {
8
        setModel(model) {
9
          this.model = model
10
        },
11
        getFormSchema() {
12
          return this.model.map(row => {
13
            const {table, form = {}, ...common} = row
14
            Object.keys(form).forEach(key => {
15
              if (key === 'type') {
16
                form.$formkit = form[key]
17
                key = '$formkit'
18
                delete form.type;
19
              }
20
              if(form[key].$ref) {
21
                const path = form[key].$ref.split('.')
22
                form[key] = path.reduce((obj, key, idx) => {
23
                  if(obj === undefined || obj[key] === undefined) return undefined
24
                  const value = obj[key]
25
                  if(idx === path.lenght - 1) delete obj[key]
26
                  return value
27
                }, common)
28
              }
29
            })
30
            return {...common, ...form}
31
          })
32
        },
33
        getTableColumns() {
34
          return this.model.map(row => {
35
            const {table = {}, form, ...common} = row
36
            Object.keys(table).forEach(key => {
37
              if(table[key].$ref) {
38
                const path = table[key].$ref.split('.')
39
                table[key] = path.reduce((obj, key, idx) => {
40
                  if(obj === undefined || obj[key] === undefined) return undefined
41
                  const value = obj[key]
42
                  if(idx === path.lenght - 1) delete obj[key]
43
                  return value
44
                }, common)
45
              }
46
            })
47
            return {...common, ...table}
48
          })
49
        }
50
    },
51
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/table.js (+96 lines)
Line 0 Link Here
1
import { defineStore } from "pinia";
2
3
import { useMainStore } from "./main"
4
import { __ } from "../i18n"
5
6
export const useTableStore = defineStore("table", {
7
  state: () => ({
8
    data: [],
9
    baseUrl: ''
10
  }),
11
  actions: {
12
    setOptions(url) {
13
      this.baseUrl = url.replace(/\/$/, '')
14
    },
15
    async add({row, headers}) {
16
      const requestHeaders = {...headers, 'Content-Type': 'application/json'}
17
      const response = await fetch(this.baseUrl, {
18
        headers: requestHeaders,
19
        method: 'POST',
20
        body: JSON.stringify(row)
21
      })
22
      const { status, statusText, ok, headers: responseHeaders } = response
23
      let data = null
24
      if(ok) {
25
        data = await response.text()
26
        try {
27
          data = JSON.parse(data)
28
        } catch(error) {}
29
      }
30
      return { status, statusText, ok, headers: responseHeaders, data }
31
    },
32
    async remove({id, headers}) {
33
      const response = await fetch(`${this.baseUrl}/${id}`, {
34
        headers,
35
        method: 'DELETE'
36
      })
37
      const { status, statusText, ok, headers: responseHeaders } = response
38
      let data = null
39
      if(ok) {
40
        data = await response.text()
41
        try {
42
          data = JSON.parse(data)
43
        } catch(error) {}
44
      }
45
      return { status, statusText, ok, headers: responseHeaders, data }
46
    },
47
    async update({id, headers, row}) {
48
      const requestHeaders = {...headers, 'Content-Type': 'application/json'}
49
      const response = await fetch(`${this.baseUrl}/${id}`, {
50
        headers: requestHeaders,
51
        method: 'PUT',
52
        body: JSON.stringify(row)
53
      })
54
      const { status, statusText, ok, headers: responseHeaders } = response
55
      let data = null
56
      if(ok) {
57
        data = await response.text()
58
        try {
59
          data = JSON.parse(data)
60
        } catch(error) {}
61
      }
62
      return { status, statusText, ok, headers: responseHeaders, data }
63
    },
64
    async get({id, headers}) {
65
      const response = await fetch(`${this.baseUrl}/${id}`, {
66
        headers,
67
      })
68
      const { status, statusText, ok, headers: responseHeaders } = response
69
      let data = null
70
      if(ok) {
71
        data = await response.text()
72
        try {
73
          data = JSON.parse(data)
74
        } catch(error) {}
75
      }
76
      return { status, statusText, ok, headers: responseHeaders, data }
77
    },
78
    async list({query=false, headers={}}) {
79
80
      const response = await fetch(`${this.baseUrl}${query?`?${query}`:''}`, {
81
          headers,
82
      })
83
84
      const { status, statusText, ok, headers: responseHeaders } = response
85
      let data = null
86
      if(ok) {
87
        data = await response.text()
88
        try {
89
          data = JSON.parse(data)
90
        } catch(error) {}
91
      }
92
      return { status, statusText, ok, headers: responseHeaders, data }
93
94
    }
95
  }
96
})
(-)a/package.json (-5 / +9 lines)
Lines 9-14 Link Here
9
  "dependencies": {
9
  "dependencies": {
10
    "@cypress/vue": "^3.1.1",
10
    "@cypress/vue": "^3.1.1",
11
    "@cypress/webpack-dev-server": "^1.8.3",
11
    "@cypress/webpack-dev-server": "^1.8.3",
12
    "@formkit/themes": "^1.0.0-beta.14",
13
    "@formkit/vue": "^1.0.0-beta.14",
12
    "@fortawesome/fontawesome-svg-core": "^6.1.0",
14
    "@fortawesome/fontawesome-svg-core": "^6.1.0",
13
    "@fortawesome/free-solid-svg-icons": "^6.0.0",
15
    "@fortawesome/free-solid-svg-icons": "^6.0.0",
14
    "@fortawesome/vue-fontawesome": "^3.0.0-5",
16
    "@fortawesome/vue-fontawesome": "^3.0.0-5",
Lines 18-23 Link Here
18
    "bootstrap": "^4.5.2",
20
    "bootstrap": "^4.5.2",
19
    "css-loader": "^6.6.0",
21
    "css-loader": "^6.6.0",
20
    "cypress": "^9.5.2",
22
    "cypress": "^9.5.2",
23
    "datatables.net": "^1.13.1",
24
    "datatables.net-vue3": "^2.0.0",
21
    "gulp": "^4.0.2",
25
    "gulp": "^4.0.2",
22
    "gulp-autoprefixer": "^4.0.0",
26
    "gulp-autoprefixer": "^4.0.0",
23
    "gulp-concat-po": "^1.0.0",
27
    "gulp-concat-po": "^1.0.0",
Lines 60-70 Link Here
60
  "author": "",
64
  "author": "",
61
  "license": "GPL-3.0",
65
  "license": "GPL-3.0",
62
  "devDependencies": {
66
  "devDependencies": {
63
    "postcss-selector-parser": "^6.0.10",
64
    "postcss": "^8.4.14",
65
    "stylelint-config-standard-scss": "^5.0.0",
66
    "stylelint-order": "^5.0.0",
67
    "stylelint": "^14.9.1",
68
    "@babel/core": "^7.17.5",
67
    "@babel/core": "^7.17.5",
69
    "@babel/preset-env": "^7.16.11",
68
    "@babel/preset-env": "^7.16.11",
70
    "@vue/compiler-sfc": "^3.2.31",
69
    "@vue/compiler-sfc": "^3.2.31",
Lines 74-79 Link Here
74
    "clean-webpack-plugin": "^4.0.0",
73
    "clean-webpack-plugin": "^4.0.0",
75
    "gulp-tap": "^1.0.1",
74
    "gulp-tap": "^1.0.1",
76
    "html-webpack-plugin": "^5.5.0",
75
    "html-webpack-plugin": "^5.5.0",
76
    "postcss": "^8.4.14",
77
    "postcss-selector-parser": "^6.0.10",
78
    "stylelint": "^14.9.1",
79
    "stylelint-config-standard-scss": "^5.0.0",
80
    "stylelint-order": "^5.0.0",
77
    "ts-loader": "^9.2.7",
81
    "ts-loader": "^9.2.7",
78
    "typescript": "^4.6.2",
82
    "typescript": "^4.6.2",
79
    "vinyl-source-stream": "^2.0.0",
83
    "vinyl-source-stream": "^2.0.0",
(-)a/t/db_dependent/Koha/ImportSources.t (+44 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2023 Theke Solutions
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Test::More tests => 1;
23
24
use Koha::ImportSources;
25
26
use t::lib::TestBuilder;
27
my $schema = Koha::Database->new->schema;
28
my $builder = t::lib::TestBuilder->new;
29
30
subtest 'patron' => sub {
31
  plan tests => 1;
32
33
  $schema->storage->txn_begin;
34
35
  my $patron = $builder->build_object({class => 'Koha::Patrons', value => {firstname => 'Afirstname'}});
36
37
  my $import_source = $builder->build_object({class => 'Koha::ImportSources', value => {name => 'import_source', patron_id => $patron->borrowernumber}});
38
39
  my $fetched_patron = $import_source->patron;
40
41
  is($fetched_patron->firstname, 'Afirstname', 'Patron matches');
42
43
  $schema->storage->txn_rollback;
44
};
(-)a/t/db_dependent/api/v1/import_sources.t (+318 lines)
Line 0 Link Here
1
#!/usr/bin/env perl
2
3
# Copyright 2023 Theke Solutions
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Test::More tests => 5;
23
use Test::Mojo;
24
use Test::Warn;
25
26
use t::lib::TestBuilder;
27
use t::lib::Mocks;
28
29
use C4::Auth;
30
use Koha::ImportSources;
31
use Koha::Database;
32
33
my $schema  = Koha::Database->new->schema;
34
my $builder = t::lib::TestBuilder->new;
35
36
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
37
38
my $t = Test::Mojo->new('Koha::REST::V1');
39
40
subtest 'list() tests' => sub {
41
42
  plan tests => 12;
43
44
  $schema->storage->txn_begin;
45
46
  my $source = $builder->build_object(
47
    {
48
      class => 'Koha::ImportSources'
49
    }
50
  );
51
  my $patron = $builder->build_object(
52
      {
53
          class => 'Koha::Patrons',
54
          value => { flags => 3 }
55
      }
56
  );
57
58
  for ( 1..10 ) {
59
      $builder->build_object(
60
        {
61
          class => 'Koha::ImportSources'
62
        }
63
      );
64
  }
65
66
  my $nonprivilegedpatron = $builder->build_object(
67
      {
68
          class => 'Koha::Patrons',
69
          value => { flags => 0 }
70
      }
71
  );
72
73
  my $password = 'thePassword123';
74
75
  $nonprivilegedpatron->set_password(
76
      { password => $password, skip_validation => 1 } );
77
  my $userid = $nonprivilegedpatron->userid;
78
79
  $t->get_ok( "//$userid:$password@/api/v1/import_sources" )
80
    ->status_is(403)
81
    ->json_is(
82
      '/error' => 'Authorization failure. Missing required permission(s).' );
83
84
  $patron->set_password( { password => $password, skip_validation => 1 } );
85
  $userid = $patron->userid;
86
87
  $t->get_ok( "//$userid:$password@/api/v1/import_sources?_per_page=10" )
88
    ->status_is( 200, 'SWAGGER3.2.2' );
89
90
  my $response_count = scalar @{ $t->tx->res->json };
91
92
  is( $response_count, 10, 'The API returns 10 sources' );
93
94
  my $id = $source->import_source_id;
95
  $t->get_ok( "//$userid:$password@/api/v1/import_sources?q={\"import_source_id\": $id}" )
96
    ->status_is(200)
97
    ->json_is( '' => [ $source->to_api ], 'SWAGGER3.3.2');
98
99
  $source->delete;
100
101
  $t->get_ok( "//$userid:$password@/api/v1/import_sources?q={\"import_source_id\": $id}" )
102
    ->status_is(200)
103
    ->json_is( '' => [] );
104
105
  $schema->storage->txn_rollback;
106
107
};
108
109
subtest 'get() tests' => sub {
110
111
  plan tests => 9;
112
113
  $schema->storage->txn_begin;
114
115
  my $source = $builder->build_object(
116
    {
117
      class => 'Koha::ImportSources'
118
    }
119
  );
120
  my $patron = $builder->build_object(
121
      {
122
          class => 'Koha::Patrons',
123
          value => { flags => 3 }
124
      }
125
  );
126
127
  my $nonprivilegedpatron = $builder->build_object(
128
      {
129
          class => 'Koha::Patrons',
130
          value => { flags => 0 }
131
      }
132
  );
133
134
  my $password = 'thePassword123';
135
136
  $nonprivilegedpatron->set_password(
137
      { password => $password, skip_validation => 1 } );
138
  my $userid = $nonprivilegedpatron->userid;
139
140
  my $id = $source->import_source_id;
141
142
  $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" )
143
    ->status_is(403)
144
    ->json_is(
145
      '/error' => 'Authorization failure. Missing required permission(s).' );
146
147
  $patron->set_password( { password => $password, skip_validation => 1 } );
148
  $userid = $patron->userid;
149
150
  $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" )
151
    ->status_is( 200, 'SWAGGER3.2.2' )
152
    ->json_is( '' => $source->to_api, 'SWAGGER3.3.2' );
153
154
  $source->delete;
155
156
  $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" )
157
    ->status_is(404)
158
    ->json_is( '/error' => 'Import source not found' );
159
160
  $schema->storage->txn_rollback;
161
162
};
163
164
subtest 'delete() tests' => sub {
165
166
  plan tests => 6;
167
168
  $schema->storage->txn_begin;
169
170
  my $source = $builder->build_object(
171
    {
172
      class => 'Koha::ImportSources'
173
    }
174
  );
175
  my $patron = $builder->build_object(
176
      {
177
          class => 'Koha::Patrons',
178
          value => { flags => 3 }
179
      }
180
  );
181
182
  my $nonprivilegedpatron = $builder->build_object(
183
      {
184
          class => 'Koha::Patrons',
185
          value => { flags => 0 }
186
      }
187
  );
188
189
  my $password = 'thePassword123';
190
191
  $nonprivilegedpatron->set_password(
192
      { password => $password, skip_validation => 1 } );
193
  my $userid = $nonprivilegedpatron->userid;
194
195
  my $id = $source->import_source_id;
196
197
  $t->delete_ok( "//$userid:$password@/api/v1/import_sources/$id" )
198
    ->status_is(403)
199
    ->json_is(
200
      '/error' => 'Authorization failure. Missing required permission(s).' );
201
202
  $patron->set_password( { password => $password, skip_validation => 1 } );
203
  $userid = $patron->userid;
204
205
  $t->delete_ok( "//$userid:$password@/api/v1/import_sources/$id" )
206
    ->status_is( 204, 'SWAGGER3.2.2' );
207
208
  my $deleted_source = Koha::ImportSources->search({import_source_id => $id});
209
210
  is($deleted_source->count, 0, 'No import source found');
211
212
  $schema->storage->txn_rollback;
213
214
};
215
216
subtest 'add() tests' => sub {
217
218
  plan tests => 8;
219
220
  $schema->storage->txn_begin;
221
222
  Koha::ImportSources->delete;
223
224
  my $patron = $builder->build_object(
225
      {
226
          class => 'Koha::Patrons',
227
          value => { flags => 3 }
228
      }
229
  );
230
231
  my $nonprivilegedpatron = $builder->build_object(
232
      {
233
          class => 'Koha::Patrons',
234
          value => { flags => 0 }
235
      }
236
  );
237
238
  my $password = 'thePassword123';
239
240
  $nonprivilegedpatron->set_password(
241
      { password => $password, skip_validation => 1 } );
242
  my $userid = $nonprivilegedpatron->userid;
243
  my $patron_id = $nonprivilegedpatron->borrowernumber;
244
245
  $t->post_ok( "//$userid:$password@/api/v1/import_sources" => json => { name => 'test1', patron_id => $patron_id })
246
    ->status_is(403)
247
    ->json_is(
248
      '/error' => 'Authorization failure. Missing required permission(s).' );
249
250
  $patron->set_password( { password => $password, skip_validation => 1 } );
251
  $userid = $patron->userid;
252
253
  $t->post_ok( "//$userid:$password@/api/v1/import_sources" => json => { name => 'test1', patron_id => $patron_id })
254
    ->status_is( 201, 'SWAGGER3.2.2' )
255
    ->json_is('/name', 'test1')
256
    ->json_is('/patron_id', $patron_id);
257
258
  my $created_source = Koha::ImportSources->search->next;
259
260
  is($created_source->name, 'test1', 'Import source found');
261
262
  $schema->storage->txn_rollback;
263
264
};
265
266
subtest 'update() tests' => sub {
267
268
  plan tests => 7;
269
270
  $schema->storage->txn_begin;
271
272
  my $source = $builder->build_object(
273
    {
274
      class => 'Koha::ImportSources',
275
      value => { name => 'Oldname' }
276
    }
277
  );
278
  my $patron = $builder->build_object(
279
      {
280
          class => 'Koha::Patrons',
281
          value => { flags => 3 }
282
      }
283
  );
284
285
  my $nonprivilegedpatron = $builder->build_object(
286
      {
287
          class => 'Koha::Patrons',
288
          value => { flags => 0 }
289
      }
290
  );
291
292
  my $password = 'thePassword123';
293
294
  $nonprivilegedpatron->set_password(
295
      { password => $password, skip_validation => 1 } );
296
  my $userid = $nonprivilegedpatron->userid;
297
298
  my $id = $source->import_source_id;
299
300
  $t->put_ok( "//$userid:$password@/api/v1/import_sources/$id" => json => { name => 'Newname', patron_id => $source->patron_id } )
301
    ->status_is(403)
302
    ->json_is(
303
      '/error' => 'Authorization failure. Missing required permission(s).' );
304
305
  $patron->set_password( { password => $password, skip_validation => 1 } );
306
  $userid = $patron->userid;
307
308
  $t->put_ok( "//$userid:$password@/api/v1/import_sources/$id" => json => { name => 'Newname', patron_id => $source->patron_id } )
309
    ->status_is( 200, 'SWAGGER3.2.2' )
310
    ->json_is('/name', 'Newname');
311
312
  my $updated_source = Koha::ImportSources->find($id);
313
314
  is($updated_source->name, 'Newname', 'Import source updated');
315
316
  $schema->storage->txn_rollback;
317
318
};
(-)a/webpack.config.js (-1 / +1 lines)
Lines 6-11 const webpack = require('webpack'); Link Here
6
module.exports = {
6
module.exports = {
7
  entry: {
7
  entry: {
8
    erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/main-erm.ts",
8
    erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/main-erm.ts",
9
    "import-source": "./koha-tmpl/intranet-tmpl/prog/js/vue/import-source/main.ts"
9
  },
10
  },
10
  output: {
11
  output: {
11
    filename: "[name].js",
12
    filename: "[name].js",
12
- 

Return to bug 32607