From 44359be1608b5bf7f9469c3a1af5ce80bc4075a2 Mon Sep 17 00:00:00 2001 From: Bertrand Zuchuat Date: Fri, 8 Jan 2021 13:06:22 +0100 Subject: [PATCH] documents: rewrite public holdings/items view in Angular * Implements the holdings section of the document detailed view of the public interface to improve the user experience when loading holdings with lots of items. With the JINJA templates, the performance is very bad. Angular allows to lazy load data and will make easier to add dynamic interaction between the user and the interface. * Adds pagination on holdings of the professional interface. * Closes rero/rero-ils#1401 * Closes rero/rero-ils#1563 * Closes rero/rero-ils#1577 Co-Authored-by: Bertrand Zuchuat --- angular.json | 90 +++++++++++++ package.json | 3 +- .../change-password-form.component.ts | 1 - .../holding/holding.component.html | 2 +- .../holdings/holdings.component.html | 7 + .../holdings/holdings.component.ts | 113 +++++++++++++--- projects/admin/src/manual_translations.ts | 2 + .../public-holdings-items/.browserslistrc | 12 ++ .../e2e/protractor.conf.js | 32 +++++ .../e2e/src/app.e2e-spec.ts | 23 ++++ .../public-holdings-items/e2e/src/app.po.ts | 11 ++ .../public-holdings-items/e2e/tsconfig.json | 13 ++ projects/public-holdings-items/karma.conf.js | 37 ++++++ .../app/app-config-service.service.spec.ts | 32 +++++ .../src/app/app-config-service.service.ts | 45 +++++++ .../src/app/app-initializer.service.spec.ts | 47 +++++++ .../src/app/app-initializer.service.ts | 60 +++++++++ .../src/app/app.module.ts | 112 ++++++++++++++++ .../src/environments/environment.prod.ts | 11 ++ .../src/environments/environment.ts | 24 ++++ .../public-holdings-items/src/favicon.ico | Bin 0 -> 5430 bytes projects/public-holdings-items/src/index.html | 55 ++++++++ projects/public-holdings-items/src/main.ts | 29 ++++ .../public-holdings-items/src/polyfills.ts | 63 +++++++++ projects/public-holdings-items/src/test.ts | 37 ++++++ .../public-holdings-items/tsconfig.app.json | 14 ++ .../public-holdings-items/tsconfig.spec.json | 18 +++ projects/public-holdings-items/tslint.json | 17 +++ .../src/app/api/holdings-api.service.spec.ts | 88 +++++++++++++ .../src/app/api/holdings-api.service.ts | 66 ++++++++++ .../src/app/api/item-api.service.spec.ts | 114 ++++++++++++++++ .../src/app/api/item-api.service.ts | 116 ++++++++++++++++ .../src/app/api/location-api.service.spec.ts | 63 +++++++++ .../src/app/api/location-api.service.ts | 55 ++++++++ .../document-detail/book/book.component.html | 22 ++++ .../book/book.component.spec.ts | 86 ++++++++++++ .../document-detail/book/book.component.ts | 86 ++++++++++++ .../holdings-items.component.html | 20 +++ .../holdings-items.component.spec.ts | 45 +++++++ .../holdings-items.component.ts | 34 +++++ .../holdings/holding/holding.component.html | 103 +++++++++++++++ .../holding/holding.component.spec.ts | 113 ++++++++++++++++ .../holdings/holding/holding.component.ts | 49 +++++++ .../holdings/holdings.component.html | 27 ++++ .../holdings/holdings.component.spec.ts | 87 ++++++++++++ .../holdings/holdings.component.ts | 106 +++++++++++++++ .../holdings/items/items.component.html | 25 ++++ .../holdings/items/items.component.spec.ts | 87 ++++++++++++ .../holdings/items/items.component.ts | 110 ++++++++++++++++ .../document-detail/item/item.component.html | 77 +++++++++++ .../item/item.component.spec.ts | 118 +++++++++++++++++ .../document-detail/item/item.component.ts | 45 +++++++ .../pickup-location.component.html | 45 +++++++ .../pickup-location.component.spec.ts | 95 ++++++++++++++ .../pickup-location.component.ts | 124 ++++++++++++++++++ .../request/request.component.html | 35 +++++ .../request/request.component.spec.ts | 82 ++++++++++++ .../request/request.component.ts | 74 +++++++++++ .../src/app/manual_translations.ts | 4 + .../src/app/pipe/notes-filter.pipe.spec.ts | 47 +++++++ .../src/app/pipe/notes-filter.pipe.ts | 38 ++++++ projects/public-search/src/app/record.ts | 23 ++++ .../src/app/user.service.spec.ts | 49 +++++++ .../public-search/src/app/user.service.ts | 45 +++++++ 64 files changed, 3292 insertions(+), 21 deletions(-) create mode 100644 projects/public-holdings-items/.browserslistrc create mode 100644 projects/public-holdings-items/e2e/protractor.conf.js create mode 100644 projects/public-holdings-items/e2e/src/app.e2e-spec.ts create mode 100644 projects/public-holdings-items/e2e/src/app.po.ts create mode 100644 projects/public-holdings-items/e2e/tsconfig.json create mode 100644 projects/public-holdings-items/karma.conf.js create mode 100644 projects/public-holdings-items/src/app/app-config-service.service.spec.ts create mode 100644 projects/public-holdings-items/src/app/app-config-service.service.ts create mode 100644 projects/public-holdings-items/src/app/app-initializer.service.spec.ts create mode 100644 projects/public-holdings-items/src/app/app-initializer.service.ts create mode 100644 projects/public-holdings-items/src/app/app.module.ts create mode 100644 projects/public-holdings-items/src/environments/environment.prod.ts create mode 100644 projects/public-holdings-items/src/environments/environment.ts create mode 100644 projects/public-holdings-items/src/favicon.ico create mode 100644 projects/public-holdings-items/src/index.html create mode 100644 projects/public-holdings-items/src/main.ts create mode 100644 projects/public-holdings-items/src/polyfills.ts create mode 100644 projects/public-holdings-items/src/test.ts create mode 100644 projects/public-holdings-items/tsconfig.app.json create mode 100644 projects/public-holdings-items/tsconfig.spec.json create mode 100644 projects/public-holdings-items/tslint.json create mode 100644 projects/public-search/src/app/api/holdings-api.service.spec.ts create mode 100644 projects/public-search/src/app/api/holdings-api.service.ts create mode 100644 projects/public-search/src/app/api/item-api.service.spec.ts create mode 100644 projects/public-search/src/app/api/item-api.service.ts create mode 100644 projects/public-search/src/app/api/location-api.service.spec.ts create mode 100644 projects/public-search/src/app/api/location-api.service.ts create mode 100644 projects/public-search/src/app/document-detail/book/book.component.html create mode 100644 projects/public-search/src/app/document-detail/book/book.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/book/book.component.ts create mode 100644 projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.html create mode 100644 projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.ts create mode 100644 projects/public-search/src/app/document-detail/holdings/holding/holding.component.html create mode 100644 projects/public-search/src/app/document-detail/holdings/holding/holding.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/holdings/holding/holding.component.ts create mode 100644 projects/public-search/src/app/document-detail/holdings/holdings.component.html create mode 100644 projects/public-search/src/app/document-detail/holdings/holdings.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/holdings/holdings.component.ts create mode 100644 projects/public-search/src/app/document-detail/holdings/items/items.component.html create mode 100644 projects/public-search/src/app/document-detail/holdings/items/items.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/holdings/items/items.component.ts create mode 100644 projects/public-search/src/app/document-detail/item/item.component.html create mode 100644 projects/public-search/src/app/document-detail/item/item.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/item/item.component.ts create mode 100644 projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.html create mode 100644 projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.ts create mode 100644 projects/public-search/src/app/document-detail/request/request.component.html create mode 100644 projects/public-search/src/app/document-detail/request/request.component.spec.ts create mode 100644 projects/public-search/src/app/document-detail/request/request.component.ts create mode 100644 projects/public-search/src/app/pipe/notes-filter.pipe.spec.ts create mode 100644 projects/public-search/src/app/pipe/notes-filter.pipe.ts create mode 100644 projects/public-search/src/app/record.ts create mode 100644 projects/public-search/src/app/user.service.spec.ts create mode 100644 projects/public-search/src/app/user.service.ts diff --git a/angular.json b/angular.json index c457846dd..b69a97d02 100644 --- a/angular.json +++ b/angular.json @@ -116,6 +116,96 @@ } } }, + "public-holdings-items": { + "projectType": "application", + "schematics": {}, + "root": "projects/public-holdings-items", + "sourceRoot": "projects/public-holdings-items/src", + "prefix": "app", + "architect": { + "build": { + "builder": "ngx-build-plus:build", + "options": { + "outputPath": "build/dist/public-holdings-items", + "index": "projects/public-holdings-items/src/index.html", + "main": "projects/public-holdings-items/src/main.ts", + "polyfills": "projects/public-holdings-items/src/polyfills.ts", + "tsConfig": "projects/public-holdings-items/tsconfig.app.json", + "aot": false, + "assets": [], + "styles": [], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "projects/public-holdings-items/src/environments/environment.ts", + "with": "projects/public-holdings-items/src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "public-holdings-items:build" + }, + "configurations": { + "production": { + "browserTarget": "public-holdings-items:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "public-holdings-items:build" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/public-holdings-items/tsconfig.app.json", + "projects/public-holdings-items/tsconfig.spec.json", + "projects/public-holdings-items/e2e/tsconfig.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + }, + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "projects/public-holdings-items/e2e/protractor.conf.js", + "devServerTarget": "public-holdings-items:serve" + }, + "configurations": { + "production": { + "devServerTarget": "public-holdings-items:serve:production" + } + } + } + } + }, "public-search": { "projectType": "application", "schematics": {}, diff --git a/package.json b/package.json index d35e5b126..0b8ab1c97 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,11 @@ "ng": "ng", "start": "ng serve", "start-admin-proxy": "ng serve admin --proxy-config proxy.conf.json", + "start-public-holdings-items-proxy": "ng serve public-holdings-items --proxy-config proxy.conf.json", "start-public-search-proxy": "ng serve public-search --proxy-config proxy.conf.json", "start-search-bar-proxy": "ng serve search-bar --proxy-config proxy.conf.json", "build-shared": "ng build --configuration production shared", - "build": "npm run build-shared && ng build admin --configuration production && ng build public-search --configuration production && ng build search-bar --single-bundle --prod", + "build": "npm run build-shared && ng build admin --prod && ng build public-search --prod && ng build public-holdings-items --single-bundle --prod && ng build search-bar --single-bundle --prod", "pack": "npm run build && ./scripts/dist_pkg.js -o build && cd build && npm pack", "test": "ng test", "lint": "ng lint", diff --git a/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts index 985c5bb70..9d8132d5e 100644 --- a/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts +++ b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts @@ -82,7 +82,6 @@ export class ChangePasswordFormComponent implements OnInit { this.closeModal(); }, (resp) => { - console.log('Error: Update Patron Password', resp); let error = this._translateService.instant('An error has occurred.'); if (resp.error && resp.error.message) { error = `${error}: (${resp.error.message})`; diff --git a/projects/admin/src/app/record/detail-view/document-detail-view/holding/holding.component.html b/projects/admin/src/app/record/detail-view/document-detail-view/holding/holding.component.html index 5f1ccc8b2..ce29cd566 100644 --- a/projects/admin/src/app/record/detail-view/document-detail-view/holding/holding.component.html +++ b/projects/admin/src/app/record/detail-view/document-detail-view/holding/holding.component.html @@ -69,7 +69,7 @@
Barcode
Status
-
Label
+
Unit
Call number
+ + + ({{ hiddenHoldings }}) + diff --git a/projects/admin/src/app/record/detail-view/document-detail-view/holdings/holdings.component.ts b/projects/admin/src/app/record/detail-view/document-detail-view/holdings/holdings.component.ts index 37a79ac24..cb9450ddd 100644 --- a/projects/admin/src/app/record/detail-view/document-detail-view/holdings/holdings.component.ts +++ b/projects/admin/src/app/record/detail-view/document-detail-view/holdings/holdings.component.ts @@ -16,9 +16,13 @@ */ import { Component, Input, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { RecordService, RecordUiService } from '@rero/ng-core'; import { Record } from '@rero/ng-core/lib/record/record'; import { UserService } from '@rero/shared'; +import { forkJoin } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { RecordPermissionService } from '../../../../service/record-permission.service'; @Component({ selector: 'admin-holdings', @@ -28,8 +32,20 @@ export class HoldingsComponent implements OnInit { /** Document */ @Input() document: any; + /** Holdings total */ + holdingsTotal = 0; + + /** Holdings per page */ + private holdingsPerPage = 5; + + /** Current page */ + page = 1; + + /** query */ + query: string; + /** Holdings */ - holdings: Array; + holdings: any[]; /** Holding type related to the parent document. */ @Input() holdingType: 'electronic' | 'serial' | 'standard'; @@ -37,37 +53,69 @@ export class HoldingsComponent implements OnInit { /** Can a new holding be added? */ canAdd = false; + /** + * Is link show more + * @return boolean + */ + get isLinkShowMore(): boolean { + return this.holdingsTotal > 0 + && ((this.page * this.holdingsPerPage) < this.holdingsTotal); + } + + /** + * Hidden holdings count + * @return string + */ + get hiddenHoldings(): string { + let count = this.holdingsTotal - (this.page * this.holdingsPerPage); + if (count < 0) { + count = 0; + } + const linkText = (count > 1) + ? '{{ counter }} hidden holdings' + : '{{ counter }} hidden holding'; + const linkTextTranslate = this._translateService.instant(linkText); + return linkTextTranslate.replace('{{ counter }}', count); + } + /** * Constructor * @param _userService - UserService * @param _recordService - RecordService * @param _recordUiService - RecordUiService + * @param _recordPermissionService - RecordPermissionService + * @param _translateService - TranslateService */ constructor( private _userService: UserService, private _recordService: RecordService, - private _recordUiService: RecordUiService + private _recordUiService: RecordUiService, + private _recordPermissionService: RecordPermissionService, + private _translateService: TranslateService ) { } /** onInit hook */ ngOnInit() { this.canAdd = (!('harvested' in this.document.metadata)); const orgPid = this._userService.user.currentOrganisation; - const query = `document.pid:${this.document.metadata.pid} AND organisation.pid:${orgPid}`; - this._recordService.getRecords( - 'holdings', - query, - 1, - RecordService.MAX_REST_RESULTS_SIZE, - undefined, - undefined, - undefined, - 'library_location' - ) - .subscribe((response: Record) => { - if (this._recordService.totalHits(response.hits.total) > 0) { - this.holdings = response.hits.hits; - } + this.query = `document.pid:${this.document.metadata.pid} AND organisation.pid:${orgPid}`; + const holdingsRecords = this._holdingsQuery(1, this.query); + const holdingsCount = this._holdingsCountQuery(this.query); + const permissionsRef = this._recordPermissionService.getPermission('holdings'); + forkJoin([holdingsRecords, holdingsCount, permissionsRef]) + .subscribe((result: [any[], number, any]) => { + this.holdings = result[0]; + this.holdingsTotal = result[1]; + const permissions = result[2]; + this.canAdd = permissions.create.can; + }); + } + + /** Show more */ + showMore() { + this.page++; + this._holdingsQuery(this.page, this.query).subscribe((holdings: any[]) => { + this.holdings = this.holdings.concat(holdings); }); } @@ -75,7 +123,7 @@ export class HoldingsComponent implements OnInit { * Delete a given holding. * @param data: object with 2 keys : * * 'holding' : the holding to delete - * * 'callBakend' : boolean if backend API should be called + * * 'callBackend' : boolean if backend API should be called */ deleteHolding(data: { holding: any, callBackend: boolean }) { const holding = data.holding; @@ -94,4 +142,33 @@ export class HoldingsComponent implements OnInit { }); } } + + /** + * Holdings count query + * @param query - string + * @return Observable + */ + private _holdingsCountQuery(query: string) { + return this._recordService.getRecords( + 'holdings', query, 1, 1, + undefined, undefined, undefined, 'library_location' + ).pipe( + map((holdings: Record) => this._recordService.totalHits(holdings.hits.total)) + ); + } + + /** + * Return a selected Holdings record + * @param page - number + * @param query - string + * @return Observable + */ + private _holdingsQuery(page: number, query: string) { + return this._recordService.getRecords( + 'holdings', query, page, this.holdingsPerPage, + undefined, undefined, undefined, 'library_location' + ).pipe(map((holdings: Record) => { + return holdings.hits.hits; + })); + } } diff --git a/projects/admin/src/manual_translations.ts b/projects/admin/src/manual_translations.ts index 909e839f5..2b7798dcb 100644 --- a/projects/admin/src/manual_translations.ts +++ b/projects/admin/src/manual_translations.ts @@ -150,3 +150,5 @@ _('{{ counter }} hidden issue'); _('{{ counter }} hidden issues'); _('{{ counter }} hidden item'); _('{{ counter }} hidden items'); +_('{{ counter }} hidden holding'); +_('{{ counter }} hidden holdings'); diff --git a/projects/public-holdings-items/.browserslistrc b/projects/public-holdings-items/.browserslistrc new file mode 100644 index 000000000..80848532e --- /dev/null +++ b/projects/public-holdings-items/.browserslistrc @@ -0,0 +1,12 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/projects/public-holdings-items/e2e/protractor.conf.js b/projects/public-holdings-items/e2e/protractor.conf.js new file mode 100644 index 000000000..73e4e6806 --- /dev/null +++ b/projects/public-holdings-items/e2e/protractor.conf.js @@ -0,0 +1,32 @@ +// @ts-check +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +/** + * @type { import("protractor").Config } + */ +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; \ No newline at end of file diff --git a/projects/public-holdings-items/e2e/src/app.e2e-spec.ts b/projects/public-holdings-items/e2e/src/app.e2e-spec.ts new file mode 100644 index 000000000..d844f41d2 --- /dev/null +++ b/projects/public-holdings-items/e2e/src/app.e2e-spec.ts @@ -0,0 +1,23 @@ +import { AppPage } from './app.po'; +import { browser, logging } from 'protractor'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getTitleText()).toEqual('Welcome to search-bar!'); + }); + + afterEach(async () => { + // Assert that there are no errors emitted from the browser + const logs = await browser.manage().logs().get(logging.Type.BROWSER); + expect(logs).not.toContain(jasmine.objectContaining({ + level: logging.Level.SEVERE, + } as logging.Entry)); + }); +}); diff --git a/projects/public-holdings-items/e2e/src/app.po.ts b/projects/public-holdings-items/e2e/src/app.po.ts new file mode 100644 index 000000000..5776aa9eb --- /dev/null +++ b/projects/public-holdings-items/e2e/src/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get(browser.baseUrl) as Promise; + } + + getTitleText() { + return element(by.css('app-root h1')).getText() as Promise; + } +} diff --git a/projects/public-holdings-items/e2e/tsconfig.json b/projects/public-holdings-items/e2e/tsconfig.json new file mode 100644 index 000000000..3d809e80f --- /dev/null +++ b/projects/public-holdings-items/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/e2e", + "module": "commonjs", + "target": "es2018", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/projects/public-holdings-items/karma.conf.js b/projects/public-holdings-items/karma.conf.js new file mode 100644 index 000000000..08910f692 --- /dev/null +++ b/projects/public-holdings-items/karma.conf.js @@ -0,0 +1,37 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/public-holdings-items'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + customLaunchers: { + ChromeHeadlessCI: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/public-holdings-items/src/app/app-config-service.service.spec.ts b/projects/public-holdings-items/src/app/app-config-service.service.spec.ts new file mode 100644 index 000000000..5f0d28e06 --- /dev/null +++ b/projects/public-holdings-items/src/app/app-config-service.service.spec.ts @@ -0,0 +1,32 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { TestBed } from '@angular/core/testing'; +import { AppConfigService } from './app-config-service.service'; + + +describe('AppConfigServiceService', () => { + let service: AppConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AppConfigService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/projects/public-holdings-items/src/app/app-config-service.service.ts b/projects/public-holdings-items/src/app/app-config-service.service.ts new file mode 100644 index 000000000..dd2e16631 --- /dev/null +++ b/projects/public-holdings-items/src/app/app-config-service.service.ts @@ -0,0 +1,45 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Injectable } from '@angular/core'; +import { CoreConfigService } from '@rero/ng-core'; + +import { environment } from '../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class AppConfigService extends CoreConfigService { + + /** Global View Name */ + globalViewName: string; + + /** Translation urls */ + translationsURLs: string[]; + + /** + * Constructor + */ + constructor() { + super(); + this.production = environment.production; + this.apiBaseUrl = environment.apiBaseUrl; + this.$refPrefix = environment.$refPrefix; + this.languages = environment.languages; + this.globalViewName = environment.globalViewName; + this.translationsURLs = environment.translationsURLs; + } +} diff --git a/projects/public-holdings-items/src/app/app-initializer.service.spec.ts b/projects/public-holdings-items/src/app/app-initializer.service.spec.ts new file mode 100644 index 000000000..b1dcc9fa5 --- /dev/null +++ b/projects/public-holdings-items/src/app/app-initializer.service.spec.ts @@ -0,0 +1,47 @@ +/* +* RERO ILS UI +* Copyright (C) 2021 RERO +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published by +* the Free Software Foundation, version 3 of the License. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { SharedConfigService, SharedModule } from '@rero/shared'; +import { AppInitializerService } from './app-initializer.service'; + + +describe('AppInitializerService', () => { + + let appInitializerService: AppInitializerService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot(), + SharedModule + ], + providers: [ + TranslateService, + SharedConfigService + ] + }); + appInitializerService = TestBed.inject(AppInitializerService); + }); + + it('should be created', () => { + expect(appInitializerService).toBeTruthy(); + }); +}); diff --git a/projects/public-holdings-items/src/app/app-initializer.service.ts b/projects/public-holdings-items/src/app/app-initializer.service.ts new file mode 100644 index 000000000..4c748bf44 --- /dev/null +++ b/projects/public-holdings-items/src/app/app-initializer.service.ts @@ -0,0 +1,60 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Injectable } from '@angular/core'; +import { TranslateService } from '@rero/ng-core'; +import { LoggedUserService, SharedConfigService } from '@rero/shared'; +import { UserService } from 'projects/public-search/src/app/user.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AppInitializerService { + + /** + * Constructor + * @param _loggedUserService - LoggedUserService + * @param _sharedConfigService - SharedConfigService + * @param _translateService - TranslateService + * @param _userService - UserService + */ + constructor( + private _loggedUserService: LoggedUserService, + private _sharedConfigService: SharedConfigService, + private _translateService: TranslateService, + private _userService: UserService + ) { } + + /** Load */ + load() { + this._initiliazeObservable(); + return new Promise((resolve) => { + this._sharedConfigService.init(); + this._userService.init(); + this._loggedUserService.load(); + resolve(true); + }); + } + + /** initialize observable */ + private _initiliazeObservable() { + // Set current language interface + this._loggedUserService.onLoggedUserLoaded$.subscribe(data => { + this._translateService.setLanguage(data.settings.language); + }); + } +} diff --git a/projects/public-holdings-items/src/app/app.module.ts b/projects/public-holdings-items/src/app/app.module.ts new file mode 100644 index 000000000..c8b6dafd8 --- /dev/null +++ b/projects/public-holdings-items/src/app/app.module.ts @@ -0,0 +1,112 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, Injector, NgModule } from '@angular/core'; +import { createCustomElement } from '@angular/elements'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule } from '@angular/router'; +import { FormlyBootstrapModule } from '@ngx-formly/bootstrap'; +import { FormlyModule } from '@ngx-formly/core'; +import { TranslateLoader as BaseTranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { CoreConfigService, CoreModule, RecordModule, TranslateLoader } from '@rero/ng-core'; +import { SharedModule } from '@rero/shared'; +import { TypeaheadModule } from 'ngx-bootstrap/typeahead'; +import { BookComponent } from 'projects/public-search/src/app/document-detail/book/book.component'; +import { HoldingsItemsComponent } from 'projects/public-search/src/app/document-detail/holdings-items/holdings-items.component'; +import { HoldingComponent } from 'projects/public-search/src/app/document-detail/holdings/holding/holding.component'; +import { HoldingsComponent } from 'projects/public-search/src/app/document-detail/holdings/holdings.component'; +import { ItemsComponent } from 'projects/public-search/src/app/document-detail/holdings/items/items.component'; +import { ItemComponent } from 'projects/public-search/src/app/document-detail/item/item.component'; +import { PickupLocationComponent } from 'projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component'; +import { RequestComponent } from 'projects/public-search/src/app/document-detail/request/request.component'; +import { NotesFilterPipe } from 'projects/public-search/src/app/pipe/notes-filter.pipe'; +import { AppConfigService } from './app-config-service.service'; +import { AppInitializerService } from './app-initializer.service'; + +/** function to instantiate the application */ +export function appInitFactory(appInitializerService: AppInitializerService) { + return () => appInitializerService.load(); +} + + +@NgModule({ + declarations: [ + BookComponent, + HoldingsItemsComponent, + HoldingsComponent, + ItemComponent, + ItemsComponent, + HoldingComponent, + RequestComponent, + PickupLocationComponent, + NotesFilterPipe + ], + imports: [ + BrowserModule, + BrowserAnimationsModule, + RouterModule.forRoot([]), + HttpClientModule, + FormsModule, + FormlyModule.forRoot(), + FormlyBootstrapModule, + CoreModule, + RecordModule, + ReactiveFormsModule, + TranslateModule.forRoot({ + loader: { + provide: BaseTranslateLoader, + useClass: TranslateLoader, + deps: [CoreConfigService, HttpClient] + }, + isolate: false + }), + TypeaheadModule.forRoot(), + SharedModule + ], + providers: [ + { provide: APP_INITIALIZER, useFactory: appInitFactory, deps: [AppInitializerService], multi: true }, + { + provide: CoreConfigService, + useClass: AppConfigService + } + ], + entryComponents: [ + BookComponent, + HoldingsItemsComponent, + HoldingsComponent, + ItemComponent, + ItemsComponent + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] +}) +export class AppModule { + + constructor(private injector: Injector) { + } + + ngDoBootstrap() { + if (!customElements.get('public-holdings-items')) { + const searchBar = createCustomElement(HoldingsItemsComponent, { injector: this.injector }); + customElements.define('public-holdings-items', searchBar); + } + } +} diff --git a/projects/public-holdings-items/src/environments/environment.prod.ts b/projects/public-holdings-items/src/environments/environment.prod.ts new file mode 100644 index 000000000..11b8b03ed --- /dev/null +++ b/projects/public-holdings-items/src/environments/environment.prod.ts @@ -0,0 +1,11 @@ +export const environment = { + production: true, + apiBaseUrl: '', + $refPrefix: 'https://ils.rero.ch', + languages: ['fr', 'de', 'it', 'en'], + globalViewName: 'global', + translationsURLs: [ + '/static/node_modules/@rero/rero-ils-ui/dist/public-search/assets/rero-ils-ui/public-search/i18n/${lang}.json', + '/api/translations/${lang}.json' + ] +}; diff --git a/projects/public-holdings-items/src/environments/environment.ts b/projects/public-holdings-items/src/environments/environment.ts new file mode 100644 index 000000000..3ec27ebff --- /dev/null +++ b/projects/public-holdings-items/src/environments/environment.ts @@ -0,0 +1,24 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false, + apiBaseUrl: '', + $refPrefix: 'https://ils.rero.ch', + languages: ['fr', 'de', 'it', 'en'], + globalViewName: 'global', + translationsURLs: [ + '/static/node_modules/@rero/rero-ils-ui/dist/public-search/assets/rero-ils-ui/public-search/i18n/${lang}.json', + '/api/translations/${lang}.json' + ] +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/projects/public-holdings-items/src/favicon.ico b/projects/public-holdings-items/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8081c7ceaf2be08bf59010158c586170d9d2d517 GIT binary patch literal 5430 zcmc(je{54#6vvCoAI3i*G5%$U7!sA3wtMZ$fH6V9C`=eXGJb@R1%(I_{vnZtpD{6n z5Pl{DmxzBDbrB>}`90e12m8T*36WoeDLA&SD_hw{H^wM!cl_RWcVA!I+x87ee975; z@4kD^=bYPn&pmG@(+JZ`rqQEKxW<}RzhW}I!|ulN=fmjVi@x{p$cC`)5$a!)X&U+blKNvN5tg=uLvuLnuqRM;Yc*swiexsoh#XPNu{9F#c`G zQLe{yWA(Y6(;>y|-efAy11k<09(@Oo1B2@0`PtZSkqK&${ zgEY}`W@t{%?9u5rF?}Y7OL{338l*JY#P!%MVQY@oqnItpZ}?s z!r?*kwuR{A@jg2Chlf0^{q*>8n5Ir~YWf*wmsh7B5&EpHfd5@xVaj&gqsdui^spyL zB|kUoblGoO7G(MuKTfa9?pGH0@QP^b#!lM1yHWLh*2iq#`C1TdrnO-d#?Oh@XV2HK zKA{`eo{--^K&MW66Lgsktfvn#cCAc*(}qsfhrvOjMGLE?`dHVipu1J3Kgr%g?cNa8 z)pkmC8DGH~fG+dlrp(5^-QBeEvkOvv#q7MBVLtm2oD^$lJZx--_=K&Ttd=-krx(Bb zcEoKJda@S!%%@`P-##$>*u%T*mh+QjV@)Qa=Mk1?#zLk+M4tIt%}wagT{5J%!tXAE;r{@=bb%nNVxvI+C+$t?!VJ@0d@HIyMJTI{vEw0Ul ze(ha!e&qANbTL1ZneNl45t=#Ot??C0MHjjgY8%*mGisN|S6%g3;Hlx#fMNcL<87MW zZ>6moo1YD?P!fJ#Jb(4)_cc50X5n0KoDYfdPoL^iV`k&o{LPyaoqMqk92wVM#_O0l z09$(A-D+gVIlq4TA&{1T@BsUH`Bm=r#l$Z51J-U&F32+hfUP-iLo=jg7Xmy+WLq6_tWv&`wDlz#`&)Jp~iQf zZP)tu>}pIIJKuw+$&t}GQuqMd%Z>0?t%&BM&Wo^4P^Y z)c6h^f2R>X8*}q|bblAF?@;%?2>$y+cMQbN{X$)^R>vtNq_5AB|0N5U*d^T?X9{xQnJYeU{ zoZL#obI;~Pp95f1`%X3D$Mh*4^?O?IT~7HqlWguezmg?Ybq|7>qQ(@pPHbE9V?f|( z+0xo!#m@Np9PljsyxBY-UA*{U*la#8Wz2sO|48_-5t8%_!n?S$zlGe+NA%?vmxjS- zHE5O3ZarU=X}$7>;Okp(UWXJxI%G_J-@IH;%5#Rt$(WUX?6*Ux!IRd$dLP6+SmPn= z8zjm4jGjN772R{FGkXwcNv8GBcZI#@Y2m{RNF_w8(Z%^A*!bS*!}s6sh*NnURytky humW;*g7R+&|Ledvc-. +--> + + + + + SearchBar + + + + + + + + + +
+
+

Document holdings items

+
+
+ +
+
+
+ + diff --git a/projects/public-holdings-items/src/main.ts b/projects/public-holdings-items/src/main.ts new file mode 100644 index 000000000..f62156be9 --- /dev/null +++ b/projects/public-holdings-items/src/main.ts @@ -0,0 +1,29 @@ +/* + * RERO ILS UI + * Copyright (C) 2019 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/projects/public-holdings-items/src/polyfills.ts b/projects/public-holdings-items/src/polyfills.ts new file mode 100644 index 000000000..aa665d6b8 --- /dev/null +++ b/projects/public-holdings-items/src/polyfills.ts @@ -0,0 +1,63 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/projects/public-holdings-items/src/test.ts b/projects/public-holdings-items/src/test.ts new file mode 100644 index 000000000..753b5f8c2 --- /dev/null +++ b/projects/public-holdings-items/src/test.ts @@ -0,0 +1,37 @@ +/* + * RERO ILS UI + * Copyright (C) 2019 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/projects/public-holdings-items/tsconfig.app.json b/projects/public-holdings-items/tsconfig.app.json new file mode 100644 index 000000000..57fc3cbc8 --- /dev/null +++ b/projects/public-holdings-items/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/test.ts", + "src/**/*.spec.ts" + ] +} diff --git a/projects/public-holdings-items/tsconfig.spec.json b/projects/public-holdings-items/tsconfig.spec.json new file mode 100644 index 000000000..a8ce1d396 --- /dev/null +++ b/projects/public-holdings-items/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/projects/public-holdings-items/tslint.json b/projects/public-holdings-items/tslint.json new file mode 100644 index 000000000..19e8161a0 --- /dev/null +++ b/projects/public-holdings-items/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ] + } +} diff --git a/projects/public-search/src/app/api/holdings-api.service.spec.ts b/projects/public-search/src/app/api/holdings-api.service.spec.ts new file mode 100644 index 000000000..7a28c68e2 --- /dev/null +++ b/projects/public-search/src/app/api/holdings-api.service.spec.ts @@ -0,0 +1,88 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { HoldingsApiService } from './holdings-api.service'; +import { RecordService } from '@rero/ng-core'; +import { HttpClient } from '@angular/common/http'; +import { QueryResponse } from '../record'; + + +describe('HoldingsService', () => { + + let service: HoldingsApiService; + + const record = { + medatadata: { + pid: '1', + name: 'holding name' + } + }; + + const emptyRecords = { + aggregations: {}, + hits: { + total: { + relation: 'eq', + value: 1 + }, + hits: [ + record + ] + }, + links: {} + }; + + const holdingsPids = ['100', '120']; + + const recordServiceSpy = jasmine.createSpyObj('RecordService', ['getRecords', 'totalHits']); + recordServiceSpy.getRecords.and.returnValue(of(emptyRecords)); + recordServiceSpy.totalHits.and.returnValue(1); + + const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); + httpClientSpy.get.and.returnValue(of(holdingsPids)); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + { provide: RecordService, useValue: recordServiceSpy }, + { provide: HttpClient, useValue: httpClientSpy } + ] + }); + service = TestBed.inject(HoldingsApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return a set of Holdings', () => { + service.getHoldingsByDocumentPidAndViewcode('1', 'global', 1).subscribe((result: QueryResponse) => { + expect(result.hits[0]).toEqual(record); + }); + }); + + it('should return a set of Holdings Pids', () => { + service.getHoldingsPidsByDocumentPidAndViewcode('1', 'global').subscribe((result: string[]) => { + expect(result).toEqual(holdingsPids); + }); + }); +}); diff --git a/projects/public-search/src/app/api/holdings-api.service.ts b/projects/public-search/src/app/api/holdings-api.service.ts new file mode 100644 index 000000000..a592ae6f8 --- /dev/null +++ b/projects/public-search/src/app/api/holdings-api.service.ts @@ -0,0 +1,66 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Record, RecordService } from '@rero/ng-core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { QueryResponse } from '../record'; + +@Injectable({ + providedIn: 'root' +}) +export class HoldingsApiService { + + /** Http headers */ + private _headers = { + Accept: 'application/rero+json, application/json' + }; + + /** + * Constructor + * @param _recordService - RecordService + * @param _httpClient - HttpClient + */ + constructor( + private _recordService: RecordService, + private _httpClient: HttpClient + ) {} + + /** + * Get Holdings by document pid and viewcode + * @param documentPid - string + * @param viewcode - string + * @return Observable + */ + getHoldingsByDocumentPidAndViewcode( + documentPid: string, viewcode: string, page: number, itemsPerPage: number = 5): Observable { + return this._recordService + .getRecords('holdings', `document.pid:${documentPid}`, page, itemsPerPage, undefined, { view: viewcode }, this._headers) + .pipe(map((response: Record) => response.hits)); + } + + /** + * Get Holdings pids by document pid and viewcode + * @param documentPid - string + * @param viewcode - string + * @return Observable + */ + getHoldingsPidsByDocumentPidAndViewcode(documentPid: string, viewcode: string): Observable { + return this._httpClient.get(`/api/holding/pids/${documentPid}?view=${viewcode}`); + } +} diff --git a/projects/public-search/src/app/api/item-api.service.spec.ts b/projects/public-search/src/app/api/item-api.service.spec.ts new file mode 100644 index 000000000..173ba245c --- /dev/null +++ b/projects/public-search/src/app/api/item-api.service.spec.ts @@ -0,0 +1,114 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { RecordService } from '@rero/ng-core'; +import { of } from 'rxjs'; +import { QueryResponse } from '../record'; +import { HoldingsApiService } from './holdings-api.service'; +import { ItemApiService } from './item-api.service'; + + +describe('ItemService', () => { + let service: ItemApiService; + + const record = { + medatadata: { + pid: '1', + name: 'item name' + } + }; + + const emptyRecords = { + aggregations: {}, + hits: { + total: { + relation: 'eq', + value: 1 + }, + hits: [ + record + ] + }, + links: {} + }; + + const canRequest = { + can: true, + reasons: [] + }; + + const request = { + request: {}, + metadata: {} + }; + + const recordServiceSpy = jasmine.createSpyObj('RecordService', ['getRecords', 'totalHits']); + recordServiceSpy.getRecords.and.returnValue(of(emptyRecords)); + recordServiceSpy.totalHits.and.returnValue(1); + + const holdingsPids = ['100', '120']; + const holdingsServiceSpy = jasmine.createSpyObj('HoldingsService', ['getHoldingsPidsByDocumentPidAndViewcode']); + holdingsServiceSpy.getHoldingsPidsByDocumentPidAndViewcode.and.returnValue(of(holdingsPids)); + + const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post']); + httpClientSpy.get.and.returnValue(of(canRequest)); + httpClientSpy.post.and.returnValue(of(request)); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + { provide: RecordService, useValue: recordServiceSpy }, + { provide: HttpClient, useValue: httpClientSpy }, + { provide: HoldingsApiService, useValue: holdingsServiceSpy } + ] + }); + service = TestBed.inject(ItemApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return a set of Items by holdings pid', () => { + service.getItemsByHoldingsPidAndViewcode('1', 'global', 1).subscribe((result: QueryResponse) => { + expect(result.hits[0]).toEqual(record); + }); + }); + + it('should return a set of Items by document pid', () => { + service.getItemsByDocumentPidAndViewcode('1', 'global', 1).subscribe((result: QueryResponse) => { + expect(result.hits[0]).toEqual(record); + }); + }); + + it('should return item can request', () => { + service.canRequest('1', 'xxxxxxxx').subscribe((result: any) => { + expect(result).toEqual(canRequest); + }); + }); + + it('should return a result of request', () => { + service.request({}).subscribe((result: any) => { + expect(result).toEqual(request); + }); + }); +}); diff --git a/projects/public-search/src/app/api/item-api.service.ts b/projects/public-search/src/app/api/item-api.service.ts new file mode 100644 index 000000000..d9473b58c --- /dev/null +++ b/projects/public-search/src/app/api/item-api.service.ts @@ -0,0 +1,116 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Record, RecordService } from '@rero/ng-core'; +import { Observable } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { QueryResponse } from '../record'; +import { HoldingsApiService } from './holdings-api.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ItemApiService { + + /** Http headers */ + private _headers = { + Accept: 'application/rero+json, application/json' + }; + + /** + * Constructor + * @param _recordService - RecordService + * @param _httpClient - HttpClient + * @param _holdingsService - HoldingsService + */ + constructor( + private _recordService: RecordService, + private _httpClient: HttpClient, + private _holdingsApiService: HoldingsApiService + ) { } + + /** + * Get items by holdings pid and viewcode + * @param holdingsPid - string + * @param viewcode - string + * @param page - number + * @param itemsPerPage - number + * @return Observable + */ + getItemsByHoldingsPidAndViewcode( + holdingsPid: string, viewcode: string, page: number, itemsPerPage: number = 5): Observable { + return this._recordService + .getRecords( + 'items', `holding.pid:${holdingsPid}`, page, itemsPerPage, undefined, + { view: viewcode }, this._headers, 'enumeration_chronology' + ).pipe(map((response: Record) => response.hits)); + } + + /** + * Get items by holdings pids and viewcode + * @param documentPid - string + * @param viewcode - string + * @param page - number + * @param itemsPerPage - number + * @return Observable + */ + getItemsByDocumentPidAndViewcode( + documentPid: string, viewcode: string, page: number, itemsPerPage: number = 5): Observable { + return this._holdingsApiService.getHoldingsPidsByDocumentPidAndViewcode(documentPid, viewcode) + .pipe( + switchMap((holdingPids: string[]) => { + return this.getItemsByHoldingsPids(holdingPids, viewcode, page, itemsPerPage); + }) + ); + } + + /** + * Get Items by holdings pids + * @param holdingsPids - array of string + * @param viewcode - string + * @param page - number + * @param itemsPerPage -number + * @return Observable + */ + getItemsByHoldingsPids( + holdingsPids: string[], viewcode: string, page: number, itemsPerPage: number = 5): Observable { + const query = 'holding.pid:' + holdingsPids.join(' OR holding.pid:'); + return this._recordService + .getRecords('items', query, page, itemsPerPage, undefined, { view: viewcode }, this._headers) + .pipe(map((response: Record) => response.hits)); + } + + /** + * Item Can request + * @param itemPid - string + * @param patronBarcode - string + * @return Observable + */ + canRequest(itemPid: string, patronBarcode: string): Observable { + return this._httpClient.get(`/api/item/${itemPid}/can_request?patron_barcode=${patronBarcode}`); + } + + /** + * Item request + * @param data - Object + * @return Obserable + */ + request(data: object): Observable { + return this._httpClient.post('/api/item/request', data); + } +} diff --git a/projects/public-search/src/app/api/location-api.service.spec.ts b/projects/public-search/src/app/api/location-api.service.spec.ts new file mode 100644 index 000000000..8ce5df6dc --- /dev/null +++ b/projects/public-search/src/app/api/location-api.service.spec.ts @@ -0,0 +1,63 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { LocationApiService } from './location-api.service'; + + +describe('LocationService', () => { + let service: LocationApiService; + + const locations = { + locations: [ + { pid: '1', pickup_name: 'location 1 pickup name', name: 'location 1 name' }, + { pid: '2', name: 'location 2 name' } + ] + }; + + const locationsResponse = [ + { pid: '1', name: 'location 1 pickup name' }, + { pid: '2', name: 'location 2 name' } + ]; + + const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); + httpClientSpy.get.and.returnValue(of(locations)); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + { provide: HttpClient, useValue: httpClientSpy } + ] + }); + service = TestBed.inject(LocationApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should a set of pickup locations', () => { + service.getPickupLocationsByItemId('1').subscribe(response => { + expect(response).toBeTruthy(locationsResponse); + }); + }); +}); diff --git a/projects/public-search/src/app/api/location-api.service.ts b/projects/public-search/src/app/api/location-api.service.ts new file mode 100644 index 000000000..1f68de6bb --- /dev/null +++ b/projects/public-search/src/app/api/location-api.service.ts @@ -0,0 +1,55 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class LocationApiService { + + /** + * Constructor + * @param _httpClient - HttpClient + */ + constructor( + private _httpClient: HttpClient + ) { } + + /** + * Get pickup locations by viewcode + * @param itemPid - string + * @return Observable + */ + getPickupLocationsByItemId(itemPid: string) { + return this._httpClient + .get(`/api/item/${itemPid}/pickup_locations`) + .pipe(map(result => { + const locations = []; + if (result) { + result.locations.forEach((location: any) => { + locations.push({ + pid: location.pid, + name: location.pickup_name ? location.pickup_name : location.name + }); + }); + } + return locations; + })); + } +} diff --git a/projects/public-search/src/app/document-detail/book/book.component.html b/projects/public-search/src/app/document-detail/book/book.component.html new file mode 100644 index 000000000..9b4424dce --- /dev/null +++ b/projects/public-search/src/app/document-detail/book/book.component.html @@ -0,0 +1,22 @@ + + + + + diff --git a/projects/public-search/src/app/document-detail/book/book.component.spec.ts b/projects/public-search/src/app/document-detail/book/book.component.spec.ts new file mode 100644 index 000000000..097db1ddf --- /dev/null +++ b/projects/public-search/src/app/document-detail/book/book.component.spec.ts @@ -0,0 +1,86 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { ItemApiService } from '../../api/item-api.service'; +import { QueryResponse } from '../../record'; +import { BookComponent } from './book.component'; + + +describe('BookComponent', () => { + let component: BookComponent; + let fixture: ComponentFixture; + + const records: QueryResponse = { + total: { + value: 10, + relation: 'eq' + }, + hits: [] + }; + + const recordServiceSpy = jasmine.createSpyObj('ItemService', [ + 'getItemsByDocumentPidAndViewcode' + ]); + recordServiceSpy.getItemsByDocumentPidAndViewcode.and.returnValue(of(records)); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ BookComponent ], + providers: [ + { provide: ItemApiService, useValue: recordServiceSpy } + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BookComponent); + component = fixture.componentInstance; + component.documentpid = '1'; + component.viewcode = 'global'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the link more items', () => { + component.itemsTotal = 10; + fixture.detectChanges(); + const showMore = fixture.nativeElement.querySelector('#show-more-1'); + expect(showMore.textContent.trim()).toEqual('Show more'); + }); + + it('should don\'t display the link more items', () => { + component.itemsTotal = 4; + fixture.detectChanges(); + const showMore = fixture.nativeElement.querySelector('#show-more-1'); + expect(showMore).toBeNull(); + }); +}); diff --git a/projects/public-search/src/app/document-detail/book/book.component.ts b/projects/public-search/src/app/document-detail/book/book.component.ts new file mode 100644 index 000000000..7e4445d11 --- /dev/null +++ b/projects/public-search/src/app/document-detail/book/book.component.ts @@ -0,0 +1,86 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ItemApiService } from '../../api/item-api.service'; +import { QueryResponse } from '../../record'; + +@Component({ + selector: 'public-search-book', + templateUrl: './book.component.html' +}) +export class BookComponent implements OnInit { + + /** Document pid */ + @Input() documentpid: string; + + /** View code */ + @Input() viewcode: string; + + /** Holdings per page */ + private itemsPerPage = 4; + + /** Page */ + page = 1; + + /** Array of items */ + items: any[]; + + /** Items count */ + itemsTotal = 0; + + /** + * Constructor + * @param _itemApiService - ItemApiService + */ + constructor(private _itemApiService: ItemApiService) {} + + /** OnInit hook */ + ngOnInit(): void { + this._itemsQuery(1).subscribe((response: QueryResponse) => { + this.itemsTotal = response.total.value; + this.items = response.hits; + }); + } + + /** + * Is link show more + * @return boolean + */ + get isLinkShowMore() { + return this.itemsTotal > 0 + && ((this.page * this.itemsPerPage) < this.itemsTotal); + } + + /** Show more */ + showMore() { + this.page++; + this._itemsQuery(this.page).subscribe((response: QueryResponse) => { + this.items = this.items.concat(response.hits); + }); + } + + /** + * Items query + * @param page - number + * @return Observable + */ + private _itemsQuery(page: number): Observable { + return this._itemApiService + .getItemsByDocumentPidAndViewcode(this.documentpid, this.viewcode, page, this.itemsPerPage); + } +} diff --git a/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.html b/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.html new file mode 100644 index 000000000..89c9538f4 --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.html @@ -0,0 +1,20 @@ + + + + + diff --git a/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.spec.ts b/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.spec.ts new file mode 100644 index 000000000..77cb04585 --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.spec.ts @@ -0,0 +1,45 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HoldingsItemsComponent } from './holdings-items.component'; + + +describe('HoldingsItemsComponent', () => { + let component: HoldingsItemsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HoldingsItemsComponent ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HoldingsItemsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.ts b/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.ts new file mode 100644 index 000000000..81ef8eacf --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings-items/holdings-items.component.ts @@ -0,0 +1,34 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'public-search-holdings-items', + templateUrl: './holdings-items.component.html' +}) +export class HoldingsItemsComponent { + + /** Document type */ + @Input() documenttype: string; + + /** Document pid */ + @Input() documentpid: string; + + /** View code */ + @Input() viewcode: string; + +} diff --git a/projects/public-search/src/app/document-detail/holdings/holding/holding.component.html b/projects/public-search/src/app/document-detail/holdings/holding/holding.component.html new file mode 100644 index 000000000..222794d24 --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/holding/holding.component.html @@ -0,0 +1,103 @@ + +
+
+
+ + {{ holding.metadata.library.name }}: {{ holding.metadata.location.name }} +
+
+ {{ holding.metadata.circulation_category.name }} +
+
+ + {{ itemsCount }} + {{ itemsCount | i18nPlural: { '<=1': 'item', 'other': 'items' } | translate }} + +
+
+ + + + {{ (itemsCount > 0 ? 'see collections and items' : 'no items received') | translate }} + + + + {{ (holding.metadata.available ? 'available' : 'not available') | translate }} + + +
+
+ +
+
+ + +
Call number
+
+ {{ holding.metadata.call_number }} + + | {{ holding.metadata.second_call_number }} + +
+
+ + +
Available collection
+
{{ holding.metadata.enumerationAndChronology }}
+
+ + +
Supplementary content
+
{{ holding.metadata.supplementaryContent }}
+
+ + +
Indexes
+
{{ holding.metadata.index }}
+
+ + +
Missing issues
+
{{ holding.metadata.missing_issues }}
+
+ + + + +
Note
+
+
+
+
+
+
+
+
+
+ +
+
diff --git a/projects/public-search/src/app/document-detail/holdings/holding/holding.component.spec.ts b/projects/public-search/src/app/document-detail/holdings/holding/holding.component.spec.ts new file mode 100644 index 000000000..6196e7b1a --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/holding/holding.component.spec.ts @@ -0,0 +1,113 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreModule } from '@rero/ng-core'; +import { NotesFilterPipe } from '../../../pipe/notes-filter.pipe'; +import { HoldingComponent } from './holding.component'; + + +describe('HoldingComponent', () => { + let component: HoldingComponent; + let fixture: ComponentFixture; + + const record = { + metadata: { + pid: '1', + library: { + name: 'library name' + }, + location: { + name: 'location name' + }, + circulation_category: { + name: 'default' + }, + holdings_type: 'serial', + available: true, + call_number: 'F123456', + second_call_number: 'S123456', + enumerationAndChronology: 'enum and chro', + supplementaryContent: 'sup content', + index: 'record index', + missing_issues: 'missing', + notes: [ + { type: 'general_note', content: 'public note' } + ] + } + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + CoreModule + ], + declarations: [ HoldingComponent, NotesFilterPipe ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HoldingComponent); + component = fixture.componentInstance; + component.holding = record; + component.itemsCount = 5; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display all data into the template', () => { + let data = fixture.nativeElement.querySelector('#holding-location-1'); + expect(data.textContent).toBeTruthy('library name: location name'); + + data = fixture.nativeElement.querySelector('#holding-category-name-1'); + expect(data.textContent).toBeTruthy('default'); + + data = fixture.nativeElement.querySelector('#holding-category-name-1'); + expect(data.textContent).toBeTruthy('default'); + + component.isCollapsed = true; + data = fixture.nativeElement.querySelector('#holding-available-1'); + expect(data.textContent).toBeTruthy('see collections and items'); + + data = fixture.nativeElement.querySelector('#holding-call-number-1'); + expect(data.textContent).toBeTruthy('F123456 | S123456'); + + data = fixture.nativeElement.querySelector('#holding-enum-chro-1'); + expect(data.textContent).toBeTruthy('enum and chro'); + + data = fixture.nativeElement.querySelector('#holding-sup-content-1'); + expect(data.textContent).toBeTruthy('sup content'); + + data = fixture.nativeElement.querySelector('#holding-index-1'); + expect(data.textContent).toBeTruthy('record index'); + + data = fixture.nativeElement.querySelector('#holding-missing-issues-1'); + expect(data.textContent).toBeTruthy('missing'); + + data = fixture.nativeElement.querySelector('#holding-note-1'); + expect(data.textContent).toBeTruthy('public note'); + }); +}); diff --git a/projects/public-search/src/app/document-detail/holdings/holding/holding.component.ts b/projects/public-search/src/app/document-detail/holdings/holding/holding.component.ts new file mode 100644 index 000000000..3b842980d --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/holding/holding.component.ts @@ -0,0 +1,49 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'public-search-holding', + templateUrl: './holding.component.html' +}) +export class HoldingComponent { + + /** Holdings record */ + @Input() holding: any; + + /** View code */ + @Input() viewcode: string; + + /** Is collapsed holdings */ + isCollapsed = false; + + /** Items count */ + itemsCount = 0; + + /** Authorized types of note */ + noteAuthorizedTypes: string[] = [ + 'general_note' + ]; + + /** + * Event items count + * @param event - number + */ + eItemsCount(event: number): void { + this.itemsCount = event; + } +} diff --git a/projects/public-search/src/app/document-detail/holdings/holdings.component.html b/projects/public-search/src/app/document-detail/holdings/holdings.component.html new file mode 100644 index 000000000..d9ad74aa2 --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/holdings.component.html @@ -0,0 +1,27 @@ + + +
+ +
+ + + ({{ hiddenHoldings }}) + +
diff --git a/projects/public-search/src/app/document-detail/holdings/holdings.component.spec.ts b/projects/public-search/src/app/document-detail/holdings/holdings.component.spec.ts new file mode 100644 index 000000000..a8fc8beb5 --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/holdings.component.spec.ts @@ -0,0 +1,87 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreModule } from '@rero/ng-core'; +import { of } from 'rxjs'; +import { HoldingsApiService } from '../../api/holdings-api.service'; +import { QueryResponse } from '../../record'; +import { HoldingsComponent } from './holdings.component'; + + +describe('HoldingsComponent', () => { + let component: HoldingsComponent; + let fixture: ComponentFixture; + + const records: QueryResponse = { + total: { + value: 10, + relation: 'eq' + }, + hits: [ + { pid: 1 } + ] + }; + + const recordServiceSpy = jasmine.createSpyObj('HoldingsService', [ + 'getHoldingsByDocumentPidAndViewcode' + ]); + recordServiceSpy.getHoldingsByDocumentPidAndViewcode.and.returnValue(of(records)); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + CoreModule + ], + declarations: [ HoldingsComponent ], + providers: [ + { provide: HoldingsApiService, useValue: recordServiceSpy } + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HoldingsComponent); + component = fixture.componentInstance; + component.documentpid = '1'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the link more holdings', () => { + component.holdingsTotal = 10; + fixture.detectChanges(); + const showMore = fixture.nativeElement.querySelector('#show-more-1'); + expect(showMore.textContent.trim()).toEqual('Show more'); + }); + + it('should don\'t display the link more holdings', () => { + component.holdingsTotal = 4; + fixture.detectChanges(); + const showMore = fixture.nativeElement.querySelector('#show-more-1'); + expect(showMore).toBeNull(); + }); +}); diff --git a/projects/public-search/src/app/document-detail/holdings/holdings.component.ts b/projects/public-search/src/app/document-detail/holdings/holdings.component.ts new file mode 100644 index 000000000..01c350479 --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/holdings.component.ts @@ -0,0 +1,106 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, Input, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { HoldingsApiService } from '../../api/holdings-api.service'; +import { QueryResponse } from '../../record'; + +@Component({ + selector: 'public-search-holdings', + templateUrl: './holdings.component.html' +}) +export class HoldingsComponent implements OnInit { + + /** View code */ + @Input() viewcode: string; + + /** Document pid */ + @Input() documentpid: string; + + /** Holdings total */ + holdingsTotal = 0; + + /** Holdings per page */ + private holdingsPerPage = 4; + + /** Current page */ + page = 1; + + /** Holdings records */ + holdings = []; + + /** + * Is link show more + * @return boolean + */ + get isLinkShowMore() { + return this.holdingsTotal > 0 + && ((this.page * this.holdingsPerPage) < this.holdingsTotal); + } + + /** + * Hidden holdings count + * @return string + */ + get hiddenHoldings(): string { + let count = this.holdingsTotal - (this.page * this.holdingsPerPage); + if (count < 0) { + count = 0; + } + const linkText = (count > 1) + ? '{{ counter }} hidden holdings' + : '{{ counter }} hidden holding'; + return this._translateService.instant(linkText, { counter: count}); + } + + /** + * Constructor + * @param _holdingsApiService - HoldingsApiService + * @param _translateService - TranslateService + */ + constructor( + private _holdingsApiService: HoldingsApiService, + private _translateService: TranslateService + ) { } + + /** OnInit hook */ + ngOnInit(): void { + this._HoldingsQuery(1).subscribe((response: QueryResponse) => { + this.holdingsTotal = response.total.value; + this.holdings = response.hits; + }); + } + + /** Show more */ + showMore() { + this.page++; + this._HoldingsQuery(this.page).subscribe((response: QueryResponse) => { + this.holdings = this.holdings.concat(response.hits); + }); + } + + /** + * Holdings query + * @param page - number + * @return Observable + */ + private _HoldingsQuery(page: number): Observable { + return this._holdingsApiService + .getHoldingsByDocumentPidAndViewcode(this.documentpid, this.viewcode, page, this.holdingsPerPage); + } +} diff --git a/projects/public-search/src/app/document-detail/holdings/items/items.component.html b/projects/public-search/src/app/document-detail/holdings/items/items.component.html new file mode 100644 index 000000000..2341e5cbe --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/items/items.component.html @@ -0,0 +1,25 @@ + + + + + + + ({{ hiddenItems }}) + diff --git a/projects/public-search/src/app/document-detail/holdings/items/items.component.spec.ts b/projects/public-search/src/app/document-detail/holdings/items/items.component.spec.ts new file mode 100644 index 000000000..3358353ec --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/items/items.component.spec.ts @@ -0,0 +1,87 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreModule } from '@rero/ng-core'; +import { of } from 'rxjs'; +import { ItemApiService } from '../../../api/item-api.service'; +import { QueryResponse } from '../../../record'; +import { ItemsComponent } from './items.component'; + + +describe('ItemComponent', () => { + let component: ItemsComponent; + let fixture: ComponentFixture; + + const records: QueryResponse = { + total: { + value: 10, + relation: 'eq' + }, + hits: [] + }; + + const recordServiceSpy = jasmine.createSpyObj('ItemService', [ + 'getItemsByHoldingsPidAndViewcode' + ]); + recordServiceSpy.getItemsByHoldingsPidAndViewcode.and.returnValue(of(records)); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot(), + CoreModule + ], + declarations: [ ItemsComponent ], + providers: [ + { provide: ItemApiService, useValue: recordServiceSpy } + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemsComponent); + component = fixture.componentInstance; + component.holdingpid = '1'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the link more items', () => { + component.itemsTotal = 10; + fixture.detectChanges(); + const showMore = fixture.nativeElement.querySelector('#show-more-1'); + expect(showMore.textContent.trim()).toEqual('Show more'); + }); + + it('should don\'t display the link more items', () => { + component.itemsTotal = 4; + fixture.detectChanges(); + const showMore = fixture.nativeElement.querySelector('#show-more-1'); + expect(showMore).toBeNull(); + }); +}); diff --git a/projects/public-search/src/app/document-detail/holdings/items/items.component.ts b/projects/public-search/src/app/document-detail/holdings/items/items.component.ts new file mode 100644 index 000000000..0097986cd --- /dev/null +++ b/projects/public-search/src/app/document-detail/holdings/items/items.component.ts @@ -0,0 +1,110 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { ItemApiService } from '../../../api/item-api.service'; +import { QueryResponse } from '../../../record'; + +@Component({ + selector: 'public-search-items', + templateUrl: './items.component.html' +}) +export class ItemsComponent implements OnInit { + + /** Holding pid */ + @Input() holdingpid: string; + + /** View code */ + @Input() viewcode: string; + + /** Event items count */ + @Output() eItemsCount: EventEmitter = new EventEmitter(); + + /** Items total */ + itemsTotal = 0; + + /** Items per page */ + private itemsPerPage = 4; + + /** Page */ + page = 1; + + /** Items records */ + items = []; + + /** + * Is link show more + * @return boolean + */ + get isLinkShowMore() { + return this.itemsTotal > 0 + && ((this.page * this.itemsPerPage) < this.itemsTotal); + } + + /** + * Hidden items count + * @return string + */ + get hiddenItems(): string { + let count = this.itemsTotal - (this.page * this.itemsPerPage); + if (count < 0) { + count = 0; + } + const linkText = (count > 1) + ? '{{ counter }} hidden items' + : '{{ counter }} hidden item'; + return this._translateService.instant(linkText, { counter: count}); + } + + /** + * Constructor + * @param _itemApiService - ItemApiService + * @param _translateService - TranslateService + */ + constructor( + private _itemApiService: ItemApiService, + private _translateService: TranslateService + ) { } + + /** OnInit hook */ + ngOnInit(): void { + this._ItemsQuery(1).subscribe((response: QueryResponse) => { + const total = response.total.value; + this.itemsTotal = total; + this.eItemsCount.emit(total); + this.items = response.hits; + }); + } + + /** Show more */ + showMore() { + this.page++; + this._ItemsQuery(this.page).subscribe((response: QueryResponse) => { + this.items = this.items.concat(response.hits); + }); + } + + /** + * Return a selected items by page number + * @param page - number + * @return Observable + */ + private _ItemsQuery(page: number) { + return this._itemApiService + .getItemsByHoldingsPidAndViewcode(this.holdingpid, this.viewcode, page, this.itemsPerPage); + } +} diff --git a/projects/public-search/src/app/document-detail/item/item.component.html b/projects/public-search/src/app/document-detail/item/item.component.html new file mode 100644 index 000000000..459ad7827 --- /dev/null +++ b/projects/public-search/src/app/document-detail/item/item.component.html @@ -0,0 +1,77 @@ + +
+ + +
Location
+
+ {{ item.metadata.library.name }}: + {{ item.metadata.location.name }} +
+
+ + +
Call number
+
+ {{ item.metadata.call_number }} + + | {{ item.metadata.second_call_number }} + +
+
+ + +
Temporary location
+
+ + {{ collection.title }} + {{ last ? '' : '; ' }} + +
+
+ + +
Unit
+
{{ item.metadata.enumerationAndChronology }}
+
+ +
Barcode
+
{{ item.metadata.barcode }}
+ + + +
{{ note.type | translate }}
+
+
+
+ +
Status
+
+ + + {{ 'due until' | translate }} {{ item.metadata.availability.due_date | dateTranslate :'shortDate' }} + + {{ item.metadata.availability.display_text | translate }} + + + + ({{ item.metadata.availability.request }} {{ (item.metadata.availability.request > 1 ? 'requests' : 'request') | translate }}) + +
+ + +
diff --git a/projects/public-search/src/app/document-detail/item/item.component.spec.ts b/projects/public-search/src/app/document-detail/item/item.component.spec.ts new file mode 100644 index 000000000..13473851f --- /dev/null +++ b/projects/public-search/src/app/document-detail/item/item.component.spec.ts @@ -0,0 +1,118 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { DateTranslatePipe, Nl2brPipe } from '@rero/ng-core'; +import { BsLocaleService } from 'ngx-bootstrap/datepicker'; +import { NotesFilterPipe } from '../../pipe/notes-filter.pipe'; +import { ItemComponent } from './item.component'; + +describe('ItemComponent', () => { + let component: ItemComponent; + let fixture: ComponentFixture; + + const record = { + metadata: { + pid: '1', + library: { + name: 'library name' + }, + location: { + name: 'location name' + }, + circulation_category: { + name: 'default' + }, + availability: { + request: 2, + due_date: '2021-02-01 12:00:00' + }, + barcode: 'B12222', + holdings_type: 'book', + available: true, + call_number: 'F123456', + enumerationAndChronology: 'enum and chro', + supplementaryContent: 'sup content', + index: 'record index', + missing_issues: 'missing', + notes: [ + { type: 'general_note', content: 'public note' } + ], + in_collection: [ + { pid: '1', viewcode: 'global', title: 'collection' } + ], + status: 'on_loan' + } + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [ + ItemComponent, + NotesFilterPipe, + Nl2brPipe, + DateTranslatePipe + ], + providers: [ + BsLocaleService + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemComponent); + component = fixture.componentInstance; + component.context = 'book'; + component.item = record; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display all data into the template', () => { + let data = fixture.nativeElement.querySelector('#item-location-1'); + expect(data.textContent).toBeTruthy('library name: location name'); + + data = fixture.nativeElement.querySelector('#item-call-number-1'); + expect(data.textContent).toBeTruthy('F123456'); + + data = fixture.nativeElement.querySelector('#item-call-number-1'); + expect(data.textContent).toBeTruthy('F123456'); + + data = fixture.nativeElement.querySelector('#item-location-temporary-1'); + expect(data.textContent).toBeTruthy('collection'); + + data = fixture.nativeElement.querySelector('#item-enum-chrono-1'); + expect(data.textContent).toBeTruthy('enum and chro'); + + data = fixture.nativeElement.querySelector('#item-barcode-1'); + expect(data.textContent).toBeTruthy('B12222'); + + data = fixture.nativeElement.querySelector('#item-status-1'); + expect(data.textContent).toBeTruthy('due until 2/1/21 (2 requests)'); + }); +}); diff --git a/projects/public-search/src/app/document-detail/item/item.component.ts b/projects/public-search/src/app/document-detail/item/item.component.ts new file mode 100644 index 000000000..776fe3cee --- /dev/null +++ b/projects/public-search/src/app/document-detail/item/item.component.ts @@ -0,0 +1,45 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'public-search-item', + templateUrl: './item.component.html' +}) +export class ItemComponent { + + /** Item record */ + @Input() item: any; + + /** View code */ + @Input() viewcode: string; + + /** context */ + @Input() context: string; + + /** index */ + @Input() index: number; + + /** Authorized types of note */ + noteAuthorizedTypes: string[] = [ + 'binding_note', + 'condition_note', + 'general_note', + 'patrimonial_note', + 'provenance_note' + ]; +} diff --git a/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.html b/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.html new file mode 100644 index 000000000..3807d05f3 --- /dev/null +++ b/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.html @@ -0,0 +1,45 @@ + + +
+
 
+
+
+ + + + + {{ 'Request in progress' | translate }} + +
+
+
+
+ +
+
 
+
+ +
+
+
diff --git a/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.spec.ts b/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.spec.ts new file mode 100644 index 000000000..a597f227d --- /dev/null +++ b/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.spec.ts @@ -0,0 +1,95 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormlyBootstrapModule } from '@ngx-formly/bootstrap'; +import { FormlyModule } from '@ngx-formly/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreModule } from '@rero/ng-core'; +import { of } from 'rxjs'; +import { ItemApiService } from '../../../api/item-api.service'; +import { LocationApiService } from '../../../api/location-api.service'; +import { UserService } from '../../../user.service'; +import { PickupLocationComponent } from './pickup-location.component'; + + +describe('PickupLocationComponent', () => { + let component: PickupLocationComponent; + let fixture: ComponentFixture; + + const itemRecord = { + metadata: { + pid: '1' + } + }; + + const pickupLocations = [ + { pid: '1', name: 'location 1' }, + { pid: '2', name: 'location 2' } + ]; + + const locationServiceSpy = jasmine.createSpyObj('LocationService', ['getPickupLocationsByItemId']); + locationServiceSpy.getPickupLocationsByItemId.and.returnValue(of(pickupLocations)); + + const userServiceSpy = jasmine.createSpyObj('UserService', ['']); + + const itemServiceSpy = jasmine.createSpyObj('ItemService', ['']); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + BrowserModule, + HttpClientTestingModule, + TranslateModule.forRoot(), + CoreModule, + FormsModule, + ReactiveFormsModule, + FormlyModule.forRoot(), + FormlyBootstrapModule + ], + declarations: [ PickupLocationComponent ], + providers: [ + { provide: LocationApiService, useValue: locationServiceSpy }, + { provide: UserService, useValue: userServiceSpy }, + { provide: ItemApiService, useValue: itemServiceSpy } + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PickupLocationComponent); + component = fixture.componentInstance; + component.item = itemRecord; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have the request button', () => { + const pickup = fixture.nativeElement.querySelector('#pickup-location-1'); + expect(pickup.textContent).toBeTruthy('Request'); + }); +}); diff --git a/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.ts b/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.ts new file mode 100644 index 000000000..a4541871b --- /dev/null +++ b/projects/public-search/src/app/document-detail/request/pickup-location/pickup-location.component.ts @@ -0,0 +1,124 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { TranslateService } from '@ngx-translate/core'; +import { tap } from 'rxjs/operators'; +import { ItemApiService } from '../../../api/item-api.service'; +import { LocationApiService } from '../../../api/location-api.service'; +import { UserService } from '../../../user.service'; + +@Component({ + selector: 'public-search-pickup-location', + templateUrl: './pickup-location.component.html' +}) +export class PickupLocationComponent implements OnInit { + + /** Item record */ + @Input() item: any; + + /** View code */ + @Input() viewcode: string; + + /** Form */ + form = new FormGroup({}); + fields: FormlyFieldConfig[] = []; + model: any = {}; + + /** Show form */ + showForm = true; + + /** Request in progress */ + requestInProgress = false; + + /** Item requested */ + requested = false; + + /** User message */ + requestMessage: { + success: boolean, + message: string + }; + + /** + * Construtor + * @param _locationApiService - LocationApiService + * @param _userService - UserService + * @param _itemApiService - ItemApiService + * @param _translateService - TranslateService + */ + constructor( + private _locationApiService: LocationApiService, + private _userService: UserService, + private _itemApiService: ItemApiService, + private _translateService: TranslateService + ) { } + + /** OnInit hook */ + ngOnInit(): void { + this._locationApiService + .getPickupLocationsByItemId(this.item.metadata.pid) + .subscribe((pickups: any) => { + const options = []; + pickups.forEach((pickup: any) => { + options.push({label: pickup.name, value: pickup.pid }); + }); + this.fields.push({ + key: `pickup`, + type: 'select', + templateOptions: { + label: this._translateService.instant('Pickup location'), + required: true, + options + } + }); + }); + } + + /** Submit form */ + submit() { + const user = this._userService.user; + this.requestInProgress = true; + this._itemApiService.request({ + item_pid: this.item.metadata.pid, + pickup_location_pid: this.model.pickup, + patron_pid: user.pid, + transaction_location_pid: this.model.pickup, + transaction_user_pid: user.pid + }) + .pipe(tap(() => { + this.showForm = false; + this.requestInProgress = false; + this.requested = true; + })) + .subscribe( + (response) => { + this.requestMessage = { + success: true, + message: this._translateService.instant('A request has been placed on this item.') + }; + }, + (error) => { + this.requestMessage = { + success: false, + message: this._translateService.instant('Error on this request.') + }; + } + ); + } +} diff --git a/projects/public-search/src/app/document-detail/request/request.component.html b/projects/public-search/src/app/document-detail/request/request.component.html new file mode 100644 index 000000000..34eb32832 --- /dev/null +++ b/projects/public-search/src/app/document-detail/request/request.component.html @@ -0,0 +1,35 @@ + + + + +
+
 
+
+ +
+
+
+
+ + + +
diff --git a/projects/public-search/src/app/document-detail/request/request.component.spec.ts b/projects/public-search/src/app/document-detail/request/request.component.spec.ts new file mode 100644 index 000000000..040825249 --- /dev/null +++ b/projects/public-search/src/app/document-detail/request/request.component.spec.ts @@ -0,0 +1,82 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { ItemApiService } from '../../api/item-api.service'; +import { UserService } from '../../user.service'; +import { RequestComponent } from './request.component'; + + +describe('RequestComponent', () => { + let component: RequestComponent; + let fixture: ComponentFixture; + + const userRecord = { + patron: { + barcode: 'B123456' + } + }; + + const itemRecord = { + metadata: { + pid: '1' + } + }; + + const userServiceSpy = jasmine.createSpyObj('UserService', ['']); + userServiceSpy.user = userRecord; + + const itemServiceSpy = jasmine.createSpyObj('ItemService', ['canRequest']); + itemServiceSpy.canRequest.and.returnValue(of({ can: true })); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ RequestComponent ], + providers: [ + { provide: UserService, useValue: userServiceSpy }, + { provide: ItemApiService, useValue: itemServiceSpy } + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RequestComponent); + component = fixture.componentInstance; + component.item = itemRecord; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have the request button', () => { + const showMore = fixture.nativeElement.querySelector('#item-request-1'); + expect(showMore.textContent).toBeTruthy('Request'); + }); +}); diff --git a/projects/public-search/src/app/document-detail/request/request.component.ts b/projects/public-search/src/app/document-detail/request/request.component.ts new file mode 100644 index 000000000..0eaa93f99 --- /dev/null +++ b/projects/public-search/src/app/document-detail/request/request.component.ts @@ -0,0 +1,74 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, Input, OnInit } from '@angular/core'; +import { ItemApiService } from '../../api/item-api.service'; +import { UserService } from '../../user.service'; + +@Component({ + selector: 'public-search-request', + templateUrl: './request.component.html' +}) +export class RequestComponent implements OnInit { + + /** Item record */ + @Input() item: any; + + /** View code */ + @Input() viewcode: string; + + /** Item Can request with reason(s) */ + canRequest: { + can: false, + reasons: [] + }; + + /** Request dialog */ + requestDialog = false; + + /** Patron is logged */ + get userLogged() { + return this._userService.user !== undefined; + } + + /** + * Constructor + * @param _itemApiService - ItemApiService + * @param _userService - UserService + */ + constructor( + private _itemApiService: ItemApiService, + private _userService: UserService + ) { } + + /** OnInit hook */ + ngOnInit(): void { + if ( + this._userService.user !== undefined + && 'patron' in this._userService.user + ) { + this._itemApiService.canRequest( + this.item.metadata.pid, + this._userService.user.patron.barcode + ).subscribe((can: any) => this.canRequest = can ); + } + } + + /** show Request Dialog */ + showRequestDialog() { + this.requestDialog = true; + } +} diff --git a/projects/public-search/src/app/manual_translations.ts b/projects/public-search/src/app/manual_translations.ts index 958dfb50c..1de6413c3 100644 --- a/projects/public-search/src/app/manual_translations.ts +++ b/projects/public-search/src/app/manual_translations.ts @@ -21,3 +21,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; // Document type _('other'); + +// Item count +_('item'); +_('items'); diff --git a/projects/public-search/src/app/pipe/notes-filter.pipe.spec.ts b/projects/public-search/src/app/pipe/notes-filter.pipe.spec.ts new file mode 100644 index 000000000..f9b93c438 --- /dev/null +++ b/projects/public-search/src/app/pipe/notes-filter.pipe.spec.ts @@ -0,0 +1,47 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { NotesFilterPipe } from './notes-filter.pipe'; + +describe('NotesFilterPipe', () => { + + let pipe: NotesFilterPipe; + + const notes = [ + { type: 'general_note', content: 'note general' }, + { type: 'other_note', content: 'note general' } + ]; + + const resultNotes = [ + { type: 'general_note', content: 'note general' } + ]; + + const authorizedType = [ + 'general_note' + ]; + + beforeEach(() => { + pipe = new NotesFilterPipe(); + }); + + it('create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + it('should filter the notes', () => { + expect(pipe.transform(notes, authorizedType)).toEqual(resultNotes); + }); +}); diff --git a/projects/public-search/src/app/pipe/notes-filter.pipe.ts b/projects/public-search/src/app/pipe/notes-filter.pipe.ts new file mode 100644 index 000000000..3b2f568da --- /dev/null +++ b/projects/public-search/src/app/pipe/notes-filter.pipe.ts @@ -0,0 +1,38 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'notesFilter' +}) +/** + * This pipe allows to filter the notes by type by passing + * a array of allowed types + * Example: notes | notesFilter : ['general_note', etc] + */ +export class NotesFilterPipe implements PipeTransform { + + /** + * Transform + * @param notes - array of notes + * @param authorizedType - array of authorized types + */ + transform(notes: { type: string, content: string}[], authorizedType: any[]): { type: string, content: string}[] { + return notes.filter(note => authorizedType.includes(note.type)); + } + +} diff --git a/projects/public-search/src/app/record.ts b/projects/public-search/src/app/record.ts new file mode 100644 index 000000000..e1324a3f6 --- /dev/null +++ b/projects/public-search/src/app/record.ts @@ -0,0 +1,23 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class QueryResponse { + hits: any[]; + total: { + value: number; + relation: string; + }; +} diff --git a/projects/public-search/src/app/user.service.spec.ts b/projects/public-search/src/app/user.service.spec.ts new file mode 100644 index 000000000..c4ea13036 --- /dev/null +++ b/projects/public-search/src/app/user.service.spec.ts @@ -0,0 +1,49 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { LoggedUserService } from '@rero/shared'; +import { UserService } from './user.service'; + + +describe('UserService', () => { + let service: UserService; + + const userRecord = { + metadata: { + pid: '1' + } + }; + const loggedUserServiceSpy = jasmine.createSpyObj('LoggedUserService', ['']); + loggedUserServiceSpy.onLoggedUserLoaded$ = userRecord; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + { provide: LoggedUserService, useValue: loggedUserServiceSpy} + ] + }); + service = TestBed.inject(UserService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/projects/public-search/src/app/user.service.ts b/projects/public-search/src/app/user.service.ts new file mode 100644 index 000000000..fe73503f6 --- /dev/null +++ b/projects/public-search/src/app/user.service.ts @@ -0,0 +1,45 @@ +/* + * RERO ILS UI + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Injectable } from '@angular/core'; +import { LoggedUserService } from '@rero/shared'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + /** Current user loggegd */ + private currentUser: any; + + /** Get user */ + get user() { + return this.currentUser; + } + + /** + * Constructor + * @param _loggedUserService - LoggedUserService + */ + constructor(private _loggedUserService: LoggedUserService) {} + + init() { + this._loggedUserService.onLoggedUserLoaded$ + .pipe(map(data => data.metadata ? data.metadata : undefined)) + .subscribe(user => this.currentUser = user); + } +}