diff --git a/src/app/core/services/record-api.service.ts b/src/app/core/services/record-api.service.ts index f375ac3..df2566c 100644 --- a/src/app/core/services/record-api.service.ts +++ b/src/app/core/services/record-api.service.ts @@ -29,14 +29,14 @@ import { Observable } from 'rxjs/Observable'; import { environment } from '../../../environments/environment'; import { AppConfigService } from './app-config.service'; import { CommonApiService } from './common-api.service'; -import { Ticket, RecordRevision } from '../../shared/interfaces'; +import { Ticket, RecordRevision, RecordResources } from '../../shared/interfaces'; import { ApiError } from '../../shared/classes'; import { editorApiUrl, apiUrl } from '../../shared/config'; @Injectable() export class RecordApiService extends CommonApiService { - private currentRecordApiUrl: string; + private currentRecordSaveApiUrl: string; private currentRecordEditorApiUrl: string; private currentRecordId: string; @@ -55,17 +55,21 @@ export class RecordApiService extends CommonApiService { .toPromise(); } - fetchRecord(pidType: string, pidValue: string): Promise { + fetchRecordResources(pidType: string, pidValue: string): Observable { this.currentRecordId = pidValue; - this.currentRecordApiUrl = `${apiUrl}/${pidType}/${pidValue}/db`; + this.currentRecordSaveApiUrl = `${apiUrl}/${pidType}/${pidValue}/db`; this.currentRecordEditorApiUrl = `${editorApiUrl}/${pidType}/${pidValue}`; this.newRecordFetched$.next(null); - return this.fetchUrl(this.currentRecordApiUrl); + return this.http + .get(this.currentRecordEditorApiUrl) + .map(res => res.json()) + .catch(error => Observable.throw(new ApiError(error))); + } saveRecord(record: object): Observable { return this.http - .put(this.currentRecordApiUrl, record) + .put(this.currentRecordSaveApiUrl, record) .catch(error => Observable.throw(new ApiError(error))); } @@ -125,7 +129,8 @@ export class RecordApiService extends CommonApiService { return this.http .get(`${apiUrl}/${recordType}/?q=${query}&size=200`, { headers: this.returnOnlyIdsHeaders }) .map(res => res.json()) - .map(json => json.hits.recids); + .map(json => json.hits.recids) + .catch(error => Observable.throw(new ApiError(error))); } preview(record: object): Promise { diff --git a/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.ts b/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.ts index 5b437b0..365f8e4 100644 --- a/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.ts +++ b/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.ts @@ -21,12 +21,13 @@ */ import { Component, Input, OnInit, OnChanges, SimpleChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { SchemaValidationProblems } from 'ng2-json-editor'; import { ToastrService } from 'ngx-toastr'; import { RecordApiService, AppConfigService, DomUtilsService, GlobalAppStateService } from '../../core/services'; -import { SubscriberComponent } from '../../shared/classes'; +import { RecordResources } from '../../shared/interfaces'; +import { SubscriberComponent, ApiError } from '../../shared/classes'; @Component({ selector: 're-json-editor-wrapper', @@ -47,6 +48,7 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O revision: object | undefined; constructor(private changeDetectorRef: ChangeDetectorRef, + private router: Router, private route: ActivatedRoute, private apiService: RecordApiService, private appConfigService: AppConfigService, @@ -58,9 +60,19 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O ngOnChanges(changes: SimpleChanges) { if ((changes['recordId'] || changes['recordType']) && this.recordId && this.recordType) { - // component loaded and being used by record-search + // component loaded and being used by record-search, not router this.record = undefined; // don't display old record while new is loading - this.fetch(this.recordType, this.recordId); + this.apiService + .fetchRecordResources(this.recordType, this.recordId) + .subscribe(resources => { + this.assignResourcesToProperties(resources); + }, (error: ApiError) => { + if (error.status === 403) { + this.toastrService.error(`Logged in user can not access to the record: ${this.recordType}/${this.recordId}`, 'Forbidden'); + } else { + this.toastrService.error('Could not load the record!', 'Error'); + } + }); } } @@ -69,13 +81,12 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O this.domUtilsService.fitEditorHeightFullPageOnResize(); this.domUtilsService.fitEditorHeightFullPage(); - if (!this.recordId || !this.recordType) { - // component loaded via router, @Input() aren't passed - this.route.params - .filter(params => params['recid']) + if (!this.recordId && !this.recordType) { + // component loaded via router, if @Input() aren't passed + this.route.data .takeUntil(this.isDestroyed) - .subscribe(params => { - this.fetch(params['type'], params['recid']); + .subscribe((data: { resources: RecordResources }) => { + this.assignResourcesToProperties(data.resources); }); } @@ -87,6 +98,15 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O }); } + private assignResourcesToProperties(resources: RecordResources) { + this.record = resources.record; + this.globalAppStateService.jsonBeingEdited$.next(this.record); + this.globalAppStateService.isJsonUpdated$.next(false); + this.config = this.appConfigService.getConfigForRecord(this.record); + this.schema = resources.schema; + this.changeDetectorRef.markForCheck(); + } + onRecordChange(record: object) { // update record if the edited one is not revision. if (!this.revision) { @@ -115,44 +135,4 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O this.globalAppStateService .validationProblems$.next(problems); } - - /** - * Performs api calls for a single record to be loaded - * and __assigns__ fetched data to class properties - * - * - checks permission - * - fetches record - * - fetches schema - * - * - shows toast message when any call fails - */ - private fetch(recordType: string, recordId: string) { - let loadingToastId; - this.apiService.checkEditorPermission(recordType, recordId) - .then(() => { - // TODO: move toast call out of then after https://github.com/angular/angular/pull/18352 - loadingToastId = this.toastrService.info( - `Loading ${recordType}/${recordId}`, 'Wait').toastId; - return this.apiService.fetchRecord(recordType, recordId); - }).then(json => { - this.record = json['metadata']; - this.globalAppStateService - .jsonBeingEdited$.next(this.record); - this.globalAppStateService - .isJsonUpdated$.next(false); - this.config = this.appConfigService.getConfigForRecord(this.record); - return this.apiService.fetchUrl(this.record['$schema']); - }).then(schema => { - this.toastrService.clear(loadingToastId); - this.schema = schema; - this.changeDetectorRef.markForCheck(); - }).catch(error => { - this.toastrService.clear(loadingToastId); - if (error.status === 403) { - this.toastrService.error(`Logged in user can not access to the record: ${recordType}/${recordId}`, 'Forbidden'); - } else { - this.toastrService.error('Could not load the record!', 'Error'); - } - }); - } } diff --git a/src/app/record-editor/record-editor.router.ts b/src/app/record-editor/record-editor.router.ts index c32c20c..00d6625 100644 --- a/src/app/record-editor/record-editor.router.ts +++ b/src/app/record-editor/record-editor.router.ts @@ -3,11 +3,22 @@ import { NgModule } from '@angular/core'; import { RecordSearchComponent } from './record-search'; import { JsonEditorWrapperComponent } from './json-editor-wrapper'; +import { RecordResourcesResolver } from './record-resources.resolver'; +import { RecordSearchResolver } from './record-search.resolver'; const recordEditorRoutes: Routes = [ - { path: ':type/search', component: RecordSearchComponent }, - { path: ':type/:recid', component: JsonEditorWrapperComponent } + { + path: ':type/search', + component: RecordSearchComponent, + resolve: { recids: RecordSearchResolver }, + runGuardsAndResolvers: 'paramsOrQueryParamsChange' + }, + { + path: ':type/:recid', + component: JsonEditorWrapperComponent, + resolve: { resources: RecordResourcesResolver } + } ]; @NgModule({ @@ -15,7 +26,11 @@ const recordEditorRoutes: Routes = [ RouterModule.forChild(recordEditorRoutes) ], exports: [ - RouterModule, + RouterModule + ], + providers: [ + RecordResourcesResolver, + RecordSearchResolver ] }) export class RecordEditorRouter { } diff --git a/src/app/record-editor/record-resources.resolver.ts b/src/app/record-editor/record-resources.resolver.ts new file mode 100644 index 0000000..cd7dfd3 --- /dev/null +++ b/src/app/record-editor/record-resources.resolver.ts @@ -0,0 +1,46 @@ +/* + * This file is part of record-editor. + * Copyright (C) 2018 CERN. + * + * record-editor is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * record-editor is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with record-editor; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + * In applying this license, CERN does not + * waive the privileges and immunities granted to it by virtue of its status + * as an Intergovernmental Organization or submit itself to any jurisdiction. + */ + +import { Resolve, ActivatedRouteSnapshot, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { RecordApiService } from '../core/services'; +import { RecordResources } from '../shared/interfaces'; +import { ApiError } from '../shared/classes'; + +@Injectable() +export class RecordResourcesResolver implements Resolve { + constructor(private router: Router, + private apiService: RecordApiService) { } + + resolve(route: ActivatedRouteSnapshot): Observable { + const params = route.params; + return this.apiService + .fetchRecordResources(params.type, params.recid) + .take(1) + .catch((error: ApiError) => { + this.router.navigate(['error', error.status]); + return null; + }); + } +} diff --git a/src/app/record-editor/record-search.resolver.ts b/src/app/record-editor/record-search.resolver.ts new file mode 100644 index 0000000..7a93461 --- /dev/null +++ b/src/app/record-editor/record-search.resolver.ts @@ -0,0 +1,50 @@ +/* + * This file is part of record-editor. + * Copyright (C) 2018 CERN. + * + * record-editor is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * record-editor is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with record-editor; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + * In applying this license, CERN does not + * waive the privileges and immunities granted to it by virtue of its status + * as an Intergovernmental Organization or submit itself to any jurisdiction. + */ + +import { Resolve, ActivatedRouteSnapshot, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { RecordResourcesResolver } from './record-resources.resolver'; +import { RecordSearchService } from '../core/services'; +import { RecordResources } from '../shared/interfaces'; +import { ApiError } from '../shared/classes'; + +@Injectable() +export class RecordSearchResolver implements Resolve> { + constructor(private router: Router, + private recordSearchService: RecordSearchService) { } + + resolve(route: ActivatedRouteSnapshot): Observable> { + if (!route.queryParams.query) { + return Observable.of([]); + } + + return this.recordSearchService + .search(route.params.type, route.queryParams.query) + .take(1) + .catch((error: ApiError) => { + this.router.navigate(['error', error.status]); + return null; + }); + } +} diff --git a/src/app/record-editor/record-search/record-search.component.ts b/src/app/record-editor/record-search/record-search.component.ts index 8490899..062eec0 100644 --- a/src/app/record-editor/record-search/record-search.component.ts +++ b/src/app/record-editor/record-search/record-search.component.ts @@ -43,7 +43,7 @@ interface RouteType { export class RecordSearchComponent extends SubscriberComponent implements OnInit { recordType: string; - recordCursor: number; + recordCursor = 0; foundRecordIds: Array; constructor(private route: ActivatedRoute, @@ -60,18 +60,16 @@ export class RecordSearchComponent extends SubscriberComponent implements OnInit this.changeDetectorRef.markForCheck(); }); - const searchSub = Observable.combineLatest( - this.route.params, - this.route.queryParams, - (params, queryParams) => { - return { params, queryParams }; - }).do((route: RouteType) => { - this.recordType = route.params.type; - }).filter((route: RouteType) => Boolean(route.queryParams.query)) - .switchMap((route: RouteType) => this.recordSearchService.search(route.params.type, route.queryParams.query)) + this.route.params .takeUntil(this.isDestroyed) - .subscribe(recordIds => { - this.foundRecordIds = recordIds; + .subscribe(params => { + this.recordType = params.type; + }); + + this.route.data + .takeUntil(this.isDestroyed) + .subscribe((data: { recids: Array }) => { + this.foundRecordIds = data.recids; this.changeDetectorRef.markForCheck(); }); } diff --git a/src/app/record-editor/search-bar/search-bar.component.ts b/src/app/record-editor/search-bar/search-bar.component.ts index 1ebab1d..e3d4ec9 100644 --- a/src/app/record-editor/search-bar/search-bar.component.ts +++ b/src/app/record-editor/search-bar/search-bar.component.ts @@ -113,9 +113,9 @@ export class SearchBarComponent extends SubscriberComponent implements OnInit { const query = this.query; const isQueryNumber = !isNaN(+query); if (isQueryNumber) { - this.router.navigate([`${this.recordType}/${query}`]); + this.router.navigate(['record', this.recordType, query]); } else { - this.router.navigate([`${this.recordType}/search`], { queryParams: { query } }); + this.router.navigate(['record', this.recordType, 'search'], { queryParams: { query } }); } } diff --git a/src/app/shared/interfaces/index.ts b/src/app/shared/interfaces/index.ts index 7d03ca9..c46770c 100644 --- a/src/app/shared/interfaces/index.ts +++ b/src/app/shared/interfaces/index.ts @@ -5,3 +5,4 @@ export { RecordRevision } from './record-revision'; export { SearchParams } from './search-params'; export { SavePreviewModalOptions } from './save-preview-modal-options'; export { AuthorExtractResult } from './author-extract-result'; +export { RecordResources } from './record-resources'; diff --git a/src/app/shared/interfaces/record-resources.ts b/src/app/shared/interfaces/record-resources.ts new file mode 100644 index 0000000..f29428a --- /dev/null +++ b/src/app/shared/interfaces/record-resources.ts @@ -0,0 +1,6 @@ +import { JSONSchema } from 'ng2-json-editor'; + +export interface RecordResources { + schema: JSONSchema; + record: object; +} diff --git a/src/app/shared/interfaces/search-params.ts b/src/app/shared/interfaces/search-params.ts index 1102ca1..8e9a2b9 100644 --- a/src/app/shared/interfaces/search-params.ts +++ b/src/app/shared/interfaces/search-params.ts @@ -1,3 +1,4 @@ export interface SearchParams { query: string; + cursor?: number; } diff --git a/src/polyfills.ts b/src/polyfills.ts index 96389d6..59db4ed 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -29,5 +29,6 @@ import 'rxjs/add/operator/do'; import 'rxjs/add/operator/first'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/take'; import 'rxjs/add/operator/takeUntil'; import 'rxjs/add/operator/toPromise';