From 8bad931621702110c15ff942436bc93821247374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Marie=CC=81thoz?= Date: Mon, 4 May 2020 07:19:28 +0200 Subject: [PATCH] search: use simple query for the search views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Uses AND operator by default. * Makes the search engine more robust to the query syntax errors. * Uses more simple syntax for boolean operator AND: +,OR: |, NOT: -. * Fixes suggestion highlights by escaping regex special characters. Co-Authored-by: Johnny MarieĢthoz --- .../no-cache-header.interceptor.ts | 55 ++++++++++++++++++- projects/admin/src/app/menu/menu.component.ts | 11 +++- .../app/record/editor/ref/ref.component.ts | 2 +- .../app/routes/acquisition-accounts-route.ts | 2 +- .../app/routes/acquisition-orders-route.ts | 6 +- .../admin/src/app/routes/budgets-route.ts | 15 +++-- .../app/routes/circulation-policies-route.ts | 10 +++- .../admin/src/app/routes/documents-route.ts | 4 ++ .../admin/src/app/routes/item-types-route.ts | 8 ++- .../admin/src/app/routes/libraries-route.ts | 8 ++- .../src/app/routes/patron-types-route.ts | 8 ++- .../admin/src/app/routes/patrons-route.ts | 8 ++- .../admin/src/app/routes/persons-route.ts | 6 +- .../admin/src/app/routes/vendors-route.ts | 4 ++ .../app/search-bar/search-bar.component.ts | 8 ++- .../src/app/service/routing-init.service.ts | 6 +- 16 files changed, 132 insertions(+), 29 deletions(-) diff --git a/projects/admin/src/app/interceptor/no-cache-header.interceptor.ts b/projects/admin/src/app/interceptor/no-cache-header.interceptor.ts index 3371c1dde..fd4d479d9 100644 --- a/projects/admin/src/app/interceptor/no-cache-header.interceptor.ts +++ b/projects/admin/src/app/interceptor/no-cache-header.interceptor.ts @@ -14,14 +14,63 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { HttpHandler, HttpInterceptor, HttpRequest, HttpParameterCodec, HttpParams, HttpEvent } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + + +class CustomEncoder implements HttpParameterCodec { + + /** + * Encodes a key name for a URL parameter or query-string. + * @param key The key name. + * @returns The encoded key name. + */ + encodeKey(key: string): string { + return encodeURIComponent(key); + } + + /** + * Encodes the value of a URL parameter or query-string. + * @param value The value. + * @returns The encoded value. + */ + encodeValue(value: string): string { + return encodeURIComponent(value); + } + + /** + * Decodes an encoded URL parameter or query-string key. + * @param key The encoded key name. + * @returns The decoded key name. + */ + decodeKey(key: string): string { + return decodeURIComponent(key); + } + + /** + * Decodes an encoded URL parameter or query-string value. + * @param value The encoded value. + * @returns The decoded value. + */ + decodeValue(value: string): string { + return decodeURIComponent(value); + } +} + @Injectable() export class NoCacheHeaderInterceptor implements HttpInterceptor { - intercept(req: HttpRequest, next: HttpHandler) { + intercept(req: HttpRequest, next: HttpHandler): Observable> { + + // encode URL parameters + // angular does not do it by default, + // see: https://github.com/angular/angular/issues/18261 for more details + const params = new HttpParams({ encoder: new CustomEncoder(), fromString: req.params.toString() }); + const authReq = req.clone({ + // Prevent caching in IE, in particular IE11. // See: https://support.microsoft.com/en-us/help/234067/how-to-prevent-caching-in-internet-explorer setHeaders: { @@ -29,6 +78,6 @@ export class NoCacheHeaderInterceptor implements HttpInterceptor { Pragma: 'no-cache' } }); - return next.handle(authReq); + return next.handle(authReq.clone({ params })); } } diff --git a/projects/admin/src/app/menu/menu.component.ts b/projects/admin/src/app/menu/menu.component.ts index 3417943cb..f039c40a1 100644 --- a/projects/admin/src/app/menu/menu.component.ts +++ b/projects/admin/src/app/menu/menu.component.ts @@ -23,6 +23,11 @@ import { MainTitlePipe } from '../pipe/main-title.pipe'; import { MenuService } from '../service/menu.service'; import { UserService } from '../service/user.service'; + +function escapeRegExp(data) { + return data.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + @Component({ selector: 'admin-menu', templateUrl: './menu.component.html', @@ -185,7 +190,7 @@ export class MenuComponent implements OnInit { const values = []; persons.hits.hits.map(hit => { let text = this.getPersonName(hit.metadata); - text = text.replace(new RegExp(query, 'gi'), `${query}`); + text = text.replace(new RegExp(escapeRegExp(query), 'gi'), `${query}`); values.push({ text, query: '', @@ -208,7 +213,9 @@ export class MenuComponent implements OnInit { truncate = true; text = this._mainTitlePipe.transform(hit.metadata.title).substr(0, this.maxLengthSuggestion); } - text = text.replace(new RegExp(query, 'gi'), `${query}`); + console.log(text); + text = text.replace(new RegExp(escapeRegExp(query), 'gi'), `${query}`); + console.log(text); if (truncate) { text = text + ' ...'; } diff --git a/projects/admin/src/app/record/editor/ref/ref.component.ts b/projects/admin/src/app/record/editor/ref/ref.component.ts index bf00a8ed3..9baa4a878 100644 --- a/projects/admin/src/app/record/editor/ref/ref.component.ts +++ b/projects/admin/src/app/record/editor/ref/ref.component.ts @@ -61,7 +61,7 @@ export class RefComponent extends FieldWrapper implements OnInit { .getRecord('mef', v.split('/').pop(), 1) .pipe( map(data => { - for (const source of ['rero', 'bnf', 'gnd']) { + for (const source of ['rero', 'bnf', 'gnd', 'idref']) { if ( data.metadata[source] && data.metadata[source].preferred_name_for_person diff --git a/projects/admin/src/app/routes/acquisition-accounts-route.ts b/projects/admin/src/app/routes/acquisition-accounts-route.ts index 648177b23..0453857f4 100644 --- a/projects/admin/src/app/routes/acquisition-accounts-route.ts +++ b/projects/admin/src/app/routes/acquisition-accounts-route.ts @@ -32,7 +32,7 @@ export class AcquisitionAccountsRoute extends BaseRoute implements RouteInterfac return { matcher: (url: any) => this.routeMatcher(url, this.name), children: [ - { path: 'edit/:pid', component: EditorComponent, canActivate: [ CanUpdateGuard ] }, + { path: 'edit/:pid', component: EditorComponent, canActivate: [CanUpdateGuard] }, { path: 'new', component: EditorComponent } ], data: { diff --git a/projects/admin/src/app/routes/acquisition-orders-route.ts b/projects/admin/src/app/routes/acquisition-orders-route.ts index f5d97a19d..fe925d51b 100644 --- a/projects/admin/src/app/routes/acquisition-orders-route.ts +++ b/projects/admin/src/app/routes/acquisition-orders-route.ts @@ -41,7 +41,7 @@ export class AcquisitionOrdersRoute extends BaseRoute implements RouteInterface children: [ { path: '', component: RecordSearchComponent }, { path: 'detail/:pid', component: DetailComponent }, - { path: 'edit/:pid', component: EditorComponent, canActivate: [ CanUpdateGuard ] }, + { path: 'edit/:pid', component: EditorComponent, canActivate: [CanUpdateGuard] }, { path: 'new', component: EditorComponent } ], data: { @@ -54,6 +54,10 @@ export class AcquisitionOrdersRoute extends BaseRoute implements RouteInterface detailComponent: AcquisitionOrderDetailViewComponent, canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), + // use simple query for UI search + preFilters: { + simple: 1 + }, preCreateRecord: (data: any) => { const user = this._routeToolService.userService.getCurrentUser(); data.order_date = formatDate( diff --git a/projects/admin/src/app/routes/budgets-route.ts b/projects/admin/src/app/routes/budgets-route.ts index 50124a346..440959a7f 100644 --- a/projects/admin/src/app/routes/budgets-route.ts +++ b/projects/admin/src/app/routes/budgets-route.ts @@ -39,8 +39,8 @@ export class BudgetsRoute extends BaseRoute implements RouteInterface { children: [ { path: '', component: RecordSearchComponent }, { path: 'detail/:pid', component: DetailComponent }, - { path: 'edit/:pid', component: EditorComponent, canActivate: [ CanUpdateGuard ] }, - { path: 'new', component: EditorComponent, canActivate: [ RoleGuard ], data: { roles: [ 'system_librarian' ]} } + { path: 'edit/:pid', component: EditorComponent, canActivate: [CanUpdateGuard] }, + { path: 'new', component: EditorComponent, canActivate: [RoleGuard], data: { roles: ['system_librarian'] } } ], data: { linkPrefix: 'records', @@ -53,13 +53,18 @@ export class BudgetsRoute extends BaseRoute implements RouteInterface { canAdd: () => this._routeToolService.canSystemLibrarian(), canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: () => this._routeToolService.canNot(), + // use simple query for UI search + preFilters: { + simple: 1 + }, preCreateRecord: (data: any) => { const user = this._routeToolService.userService.getCurrentUser(); data.organisation = { $ref: this._routeToolService.apiService.getRefEndpoint( - 'organisations', - user.library.organisation.pid - )}; + 'organisations', + user.library.organisation.pid + ) + }; return data; } } diff --git a/projects/admin/src/app/routes/circulation-policies-route.ts b/projects/admin/src/app/routes/circulation-policies-route.ts index 196b74ff8..89f71ca84 100644 --- a/projects/admin/src/app/routes/circulation-policies-route.ts +++ b/projects/admin/src/app/routes/circulation-policies-route.ts @@ -40,8 +40,8 @@ export class CirculationPoliciesRoute extends BaseRoute implements RouteInterfac children: [ { path: '', component: RecordSearchComponent }, { path: 'detail/:pid', component: DetailComponent }, - { path: 'edit/:pid', component: CirculationPolicyComponent, canActivate: [ CanUpdateGuard ] }, - { path: 'new', component: CirculationPolicyComponent, canActivate: [ RoleGuard ], data: { roles: [ 'system_librarian' ]} } + { path: 'edit/:pid', component: CirculationPolicyComponent, canActivate: [CanUpdateGuard] }, + { path: 'new', component: CirculationPolicyComponent, canActivate: [RoleGuard], data: { roles: ['system_librarian'] } } ], data: { linkPrefix: 'records', @@ -53,7 +53,11 @@ export class CirculationPoliciesRoute extends BaseRoute implements RouteInterfac detailComponent: CircPolicyDetailViewComponent, canAdd: () => this._routeToolService.canSystemLibrarian(), canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), - canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType) + canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), + // use simple query for UI search + preFilters: { + simple: 1 + } } ] } diff --git a/projects/admin/src/app/routes/documents-route.ts b/projects/admin/src/app/routes/documents-route.ts index e32f1b2fe..30bc75eec 100644 --- a/projects/admin/src/app/routes/documents-route.ts +++ b/projects/admin/src/app/routes/documents-route.ts @@ -55,6 +55,10 @@ export class DocumentsRoute extends BaseRoute implements RouteInterface { detailComponent: DocumentDetailViewComponent, canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), + // use simple query for UI search + preFilters: { + simple: 1 + }, preprocessRecordEditor: (record: any) => { return this.removeKey(record, '_text'); }, diff --git a/projects/admin/src/app/routes/item-types-route.ts b/projects/admin/src/app/routes/item-types-route.ts index 667b22b0c..416d3d82d 100644 --- a/projects/admin/src/app/routes/item-types-route.ts +++ b/projects/admin/src/app/routes/item-types-route.ts @@ -39,8 +39,8 @@ export class ItemTypesRoute extends BaseRoute implements RouteInterface { children: [ { path: '', component: RecordSearchComponent }, { path: 'detail/:pid', component: DetailComponent }, - { path: 'edit/:pid', component: EditorComponent, canActivate: [ CanUpdateGuard ] }, - { path: 'new', component: EditorComponent, canActivate: [ RoleGuard ], data: { roles: [ 'system_librarian' ]} } + { path: 'edit/:pid', component: EditorComponent, canActivate: [CanUpdateGuard] }, + { path: 'new', component: EditorComponent, canActivate: [RoleGuard], data: { roles: ['system_librarian'] } } ], data: { linkPrefix: 'records', @@ -53,6 +53,10 @@ export class ItemTypesRoute extends BaseRoute implements RouteInterface { canAdd: () => this._routeToolService.canSystemLibrarian(), canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), + // use simple query for UI search + preFilters: { + simple: 1 + }, preCreateRecord: (data: any) => { const user = this._routeToolService.userService.getCurrentUser(); data.organisation = { diff --git a/projects/admin/src/app/routes/libraries-route.ts b/projects/admin/src/app/routes/libraries-route.ts index ae93204eb..b67ff32b1 100644 --- a/projects/admin/src/app/routes/libraries-route.ts +++ b/projects/admin/src/app/routes/libraries-route.ts @@ -40,8 +40,8 @@ export class LibrariesRoute extends BaseRoute implements RouteInterface { children: [ { path: '', component: RecordSearchComponent }, { path: 'detail/:pid', component: DetailComponent }, - { path: 'edit/:pid', component: LibraryComponent, canActivate: [ CanUpdateGuard ] }, - { path: 'new', component: LibraryComponent, canActivate: [ RoleGuard ], data: { roles: [ 'system_librarian' ]} } + { path: 'edit/:pid', component: LibraryComponent, canActivate: [CanUpdateGuard] }, + { path: 'new', component: LibraryComponent, canActivate: [RoleGuard], data: { roles: ['system_librarian'] } } ], data: { linkPrefix: 'records', @@ -54,6 +54,10 @@ export class LibrariesRoute extends BaseRoute implements RouteInterface { canAdd: () => this._routeToolService.canSystemLibrarian(), canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), + // use simple query for UI search + preFilters: { + simple: 1 + }, preCreateRecord: (data: any) => { const user = this._routeToolService.userService.getCurrentUser(); data.organisation = { diff --git a/projects/admin/src/app/routes/patron-types-route.ts b/projects/admin/src/app/routes/patron-types-route.ts index 81028a577..bbaad9b94 100644 --- a/projects/admin/src/app/routes/patron-types-route.ts +++ b/projects/admin/src/app/routes/patron-types-route.ts @@ -40,8 +40,8 @@ export class PatronTypesRoute extends BaseRoute implements RouteInterface { children: [ { path: '', component: RecordSearchComponent }, { path: 'detail/:pid', component: DetailComponent }, - { path: 'edit/:pid', component: EditorComponent, canActivate: [ CanUpdateGuard ] }, - { path: 'new', component: EditorComponent, canActivate: [ RoleGuard ], data: { roles: [ 'system_librarian' ]} } + { path: 'edit/:pid', component: EditorComponent, canActivate: [CanUpdateGuard] }, + { path: 'new', component: EditorComponent, canActivate: [RoleGuard], data: { roles: ['system_librarian'] } } ], data: { linkPrefix: 'records', @@ -54,6 +54,10 @@ export class PatronTypesRoute extends BaseRoute implements RouteInterface { canAdd: () => this._routeToolService.canSystemLibrarian(), canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), + // use simple query for UI search + preFilters: { + simple: 1 + }, preCreateRecord: (data: any) => { const user = this._routeToolService.userService.getCurrentUser(); data.organisation = { diff --git a/projects/admin/src/app/routes/patrons-route.ts b/projects/admin/src/app/routes/patrons-route.ts index 465349853..0b2c58c1a 100644 --- a/projects/admin/src/app/routes/patrons-route.ts +++ b/projects/admin/src/app/routes/patrons-route.ts @@ -38,7 +38,7 @@ export class PatronsRoute extends BaseRoute implements RouteInterface { children: [ { path: '', component: RecordSearchComponent }, { path: 'detail/:pid', component: DetailComponent }, - { path: 'edit/:pid', component: EditorComponent, canActivate: [ CanUpdateGuard ] }, + { path: 'edit/:pid', component: EditorComponent, canActivate: [CanUpdateGuard] }, { path: 'new', component: EditorComponent } ], data: { @@ -51,7 +51,6 @@ export class PatronsRoute extends BaseRoute implements RouteInterface { detailComponent: PatronDetailViewComponent, canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), - aggregationsExpand: ['roles'], // Clean-up 'blocked_note' field content if blocked is false. postprocessRecordEditor: (record: any) => { if (record.blocked === false) { @@ -59,6 +58,11 @@ export class PatronsRoute extends BaseRoute implements RouteInterface { } return record; }, + // use simple query for UI search + preFilters: { + simple: 1 + }, + aggregationsExpand: ['roles'] } ] } diff --git a/projects/admin/src/app/routes/persons-route.ts b/projects/admin/src/app/routes/persons-route.ts index 6db080d4a..3be932198 100644 --- a/projects/admin/src/app/routes/persons-route.ts +++ b/projects/admin/src/app/routes/persons-route.ts @@ -48,7 +48,11 @@ export class PersonsRoute extends BaseRoute implements RouteInterface { label: 'Persons', component: PersonsBriefViewComponent, detailComponent: PersonDetailViewComponent, - aggregationsExpand: ['sources'] + aggregationsExpand: ['sources'], + // use simple query for UI search + preFilters: { + simple: 1 + } } ] } diff --git a/projects/admin/src/app/routes/vendors-route.ts b/projects/admin/src/app/routes/vendors-route.ts index 778a16ec8..81d5c727c 100644 --- a/projects/admin/src/app/routes/vendors-route.ts +++ b/projects/admin/src/app/routes/vendors-route.ts @@ -50,6 +50,10 @@ export class VendorsRoute extends BaseRoute implements RouteInterface { detailComponent: VendorDetailViewComponent, canUpdate: (record: any) => this._routeToolService.canUpdate(record, this.recordType), canDelete: (record: any) => this._routeToolService.canDelete(record, this.recordType), + // use simple query for UI search + preFilters: { + simple: 1 + }, preCreateRecord: (data: any) => { const user = this._routeToolService.userService.getCurrentUser(); data.organisation = { diff --git a/projects/public-search/src/app/search-bar/search-bar.component.ts b/projects/public-search/src/app/search-bar/search-bar.component.ts index 3e93acbfd..65e745d05 100644 --- a/projects/public-search/src/app/search-bar/search-bar.component.ts +++ b/projects/public-search/src/app/search-bar/search-bar.component.ts @@ -3,6 +3,10 @@ import { TranslateService } from '@ngx-translate/core'; import { MainTitlePipe } from '../../../../admin/src/app/pipe/main-title.pipe'; +function escapeRegExp(data) { + return data.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + @Component({ selector: 'public-search-search-bar', templateUrl: './search-bar.component.html', @@ -59,7 +63,7 @@ export class SearchBarComponent implements OnInit { const values = []; persons.hits.hits.map(hit => { let text = SearchBarComponent.getPersonName(hit.metadata); - text = text.replace(new RegExp(query, 'gi'), `${query}`); + text = text.replace(new RegExp(escapeRegExp(query), 'gi'), `${query}`); values.push({ text, query: '', @@ -81,7 +85,7 @@ export class SearchBarComponent implements OnInit { truncate = true; text = this._mainTitlePipe.transform(hit.metadata.title).substr(0, this.maxLengthSuggestion); } - text = text.replace(new RegExp(query, 'gi'), `${query}`); + text = text.replace(new RegExp(escapeRegExp(query), 'gi'), `${query}`); if (truncate) { text = text + ' ...'; } diff --git a/projects/public-search/src/app/service/routing-init.service.ts b/projects/public-search/src/app/service/routing-init.service.ts index 3df78b9ff..7dc4887a2 100644 --- a/projects/public-search/src/app/service/routing-init.service.ts +++ b/projects/public-search/src/app/service/routing-init.service.ts @@ -74,7 +74,8 @@ export class RoutingInitService { aggregationsExpand: ['document_type'], aggregationsBucketSize: 10, preFilters: { - view: `${viewcode}` + view: `${viewcode}`, + simple: 1 }, listHeaders: { Accept: 'application/rero+json, application/json' @@ -89,7 +90,8 @@ export class RoutingInitService { Accept: 'application/rero+json, application/json' }, preFilters: { - view: `${viewcode}` + view: `${viewcode}`, + simple: 1 } } ]