diff --git a/package.json b/package.json index ae4602b..03c1963 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@angular/router": "^4.0.0", "core-js": "^2.4.1", "font-awesome": "^4.7.0", + "idlejs": "^2.0.0", "immutable": "^3.8.1", "lodash": "4.17.2", "ng2-json-editor": "^0.24.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 91b4cfa..324c578 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -40,7 +40,7 @@ export class RavenErrorHandler implements ErrorHandler { BrowserAnimationsModule, // needed for ToastrModule HttpModule, AppRouter, - // feature-modules + CoreModule, // all core services AccordionModule.forRoot(), // ngx-toastr diff --git a/src/app/app.router.ts b/src/app/app.router.ts index 91d788f..e33c2f3 100644 --- a/src/app/app.router.ts +++ b/src/app/app.router.ts @@ -4,7 +4,10 @@ import { NgModule } from '@angular/core'; const appRoutes: Routes = [ { path: 'holdingpen', loadChildren: './holdingpen-editor/holdingpen-editor.module#HoldingpenEditorModule' }, { path: 'record', loadChildren: './record-editor/record-editor.module#RecordEditorModule' }, - { path: 'multieditor', loadChildren: './multi-editor/multi-editor.module#MultiEditorModule' } + { path: 'multieditor', loadChildren: './multi-editor/multi-editor.module#MultiEditorModule' }, + { path: 'error', loadChildren: './error/error.module#ErrorModule' }, + { path: '**', pathMatch: 'full', redirectTo: 'error' } + ]; @NgModule({ diff --git a/src/app/core/services/global-app-state.service.ts b/src/app/core/services/global-app-state.service.ts index f07414c..019a0ce 100644 --- a/src/app/core/services/global-app-state.service.ts +++ b/src/app/core/services/global-app-state.service.ts @@ -22,6 +22,7 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs/Subject'; +import { ReplaySubject } from 'rxjs/ReplaySubject'; import { SchemaValidationProblems } from 'ng2-json-editor'; import { editorConfigs } from '../../shared/config'; @@ -29,9 +30,9 @@ import { onDocumentTypeChange } from '../../shared/config/hep'; @Injectable() export class GlobalAppStateService { - readonly jsonBeingEdited$ = new Subject(); + readonly jsonBeingEdited$ = new ReplaySubject(1); - readonly isJsonUpdated$ = new Subject(); + readonly isJsonUpdated$ = new ReplaySubject(1); readonly validationProblems$ = new Subject(); readonly hasAnyValidationProblem$ = this.validationProblems$ diff --git a/src/app/core/services/record-api.service.ts b/src/app/core/services/record-api.service.ts index f375ac3..d2bc7a7 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; @@ -48,29 +48,44 @@ export class RecordApiService extends CommonApiService { super(http); } - checkEditorPermission(pidType: string, pidValue: string): Promise { - this.currentRecordEditorApiUrl = `${editorApiUrl}/${pidType}/${pidValue}`; - return this.http - .get(`${this.currentRecordEditorApiUrl}/permission`) - .toPromise(); - } - - fetchRecord(pidType: string, pidValue: string): Promise { + fetchRecordResources(pidType: string, pidValue: string): Observable { + if (this.currentRecordEditorApiUrl) { + this.unlockRecord(); + } this.currentRecordId = pidValue; - this.currentRecordApiUrl = `${apiUrl}/${pidType}/${pidValue}/db`; + this.currentRecordSaveApiUrl = `${apiUrl}/${pidType}/${pidValue}/db`; this.currentRecordEditorApiUrl = `${editorApiUrl}/${pidType}/${pidValue}`; + // TODO: remove this side effect when every action done in resolvers 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))); } - fetchRecordTickets(): Promise> { - return this.fetchUrl(`${this.currentRecordEditorApiUrl}/rt/tickets`); + lockRecord() { + this.http + .post(`${this.currentRecordEditorApiUrl}/lock/lock`, null) + .subscribe(); + } + + unlockRecord() { + this.http + .post(`${this.currentRecordEditorApiUrl}/lock/unlock`, null) + .subscribe(); + } + + fetchRecordTickets(): Observable> { + return this.http + .get(`${this.currentRecordEditorApiUrl}/rt/tickets`) + .map(res => res.json()); } createRecordTicket(ticket: Ticket): Promise<{ id: string, link: string }> { @@ -100,11 +115,10 @@ export class RecordApiService extends CommonApiService { .map((queues: Array<{ name: string }>) => queues.map(queue => queue.name)); } - fetchRevisions(): Promise> { + fetchRevisions(): Observable> { return this.http .get(`${this.currentRecordEditorApiUrl}/revisions`) - .map(res => res.json()) - .toPromise(); + .map(res => res.json()); } fetchRevisionData(transactionId: number, recUUID: string): Promise { @@ -125,7 +139,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/core/services/record-search.service.ts b/src/app/core/services/record-search.service.ts index 1572f12..c607efe 100644 --- a/src/app/core/services/record-search.service.ts +++ b/src/app/core/services/record-search.service.ts @@ -30,19 +30,27 @@ import { RecordApiService } from './record-api.service'; @Injectable() export class RecordSearchService { readonly resultCount$ = new ReplaySubject(1); - readonly cursor$ = new ReplaySubject(1); + + private lastSearchResult: Array; + private lastSearchQuery: string; constructor(private apiService: RecordApiService) { } + /** + * Performs search with side effect and simple caching + */ search(recordType: string, query: string): Observable> { - return this.apiService.searchRecord(recordType, query) - .do(results => { - this.resultCount$.next(results.length); - this.cursor$.next(0); + if (query === this.lastSearchQuery) { + return Observable.of(this.lastSearchResult); + } + + return this.apiService + .searchRecord(recordType, query) + .do((foundIds) => { + this.lastSearchResult = foundIds; + this.lastSearchQuery = query; + this.resultCount$.next(foundIds.length); }); } - setCursor(cursor: number) { - this.cursor$.next(cursor); - } } diff --git a/src/app/error/error.module.ts b/src/app/error/error.module.ts new file mode 100644 index 0000000..88e576f --- /dev/null +++ b/src/app/error/error.module.ts @@ -0,0 +1,47 @@ +/* + * 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 { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared'; + +import { ErrorRouter } from './error.router'; +import { NotFoundComponent } from './not-found'; +import { ForbiddenComponent } from './forbidden'; +import { LockedComponent } from './locked'; +import { InternalServerErrorComponent } from './internal-server-error'; + + + +@NgModule({ + imports: [ + SharedModule, + ErrorRouter + ], + declarations: [ + NotFoundComponent, + ForbiddenComponent, + LockedComponent, + InternalServerErrorComponent + ] +}) +export class ErrorModule { } diff --git a/src/app/error/error.router.ts b/src/app/error/error.router.ts new file mode 100644 index 0000000..1d3661d --- /dev/null +++ b/src/app/error/error.router.ts @@ -0,0 +1,25 @@ +import { Routes, RouterModule } from '@angular/router'; +import { NgModule } from '@angular/core'; + +import { NotFoundComponent } from './not-found'; +import { ForbiddenComponent } from './forbidden'; +import { LockedComponent } from './locked'; +import { InternalServerErrorComponent } from './internal-server-error'; + +const errorRoutes: Routes = [ + { path: '', redirectTo: '404' }, + { path: '403', component: ForbiddenComponent }, + { path: '404', component: NotFoundComponent }, + { path: '423', component: LockedComponent }, + { path: '500', component: InternalServerErrorComponent } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(errorRoutes) + ], + exports: [ + RouterModule, + ] +}) +export class ErrorRouter { } diff --git a/src/app/error/forbidden/forbidden.component.html b/src/app/error/forbidden/forbidden.component.html new file mode 100644 index 0000000..04e0e17 --- /dev/null +++ b/src/app/error/forbidden/forbidden.component.html @@ -0,0 +1 @@ +

Forbidden

\ No newline at end of file diff --git a/src/app/record-editor/record-search/record-search.component.scss b/src/app/error/forbidden/forbidden.component.scss similarity index 100% rename from src/app/record-editor/record-search/record-search.component.scss rename to src/app/error/forbidden/forbidden.component.scss diff --git a/src/app/error/forbidden/forbidden.component.ts b/src/app/error/forbidden/forbidden.component.ts new file mode 100644 index 0000000..a63f524 --- /dev/null +++ b/src/app/error/forbidden/forbidden.component.ts @@ -0,0 +1,30 @@ +/* + * 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 { Component } from '@angular/core'; + +@Component({ + selector: 're-forbidden', + templateUrl: './forbidden.component.html', + styleUrls: ['./forbidden.component.scss'] +}) +export class ForbiddenComponent { } diff --git a/src/app/error/forbidden/index.ts b/src/app/error/forbidden/index.ts new file mode 100644 index 0000000..f088d0e --- /dev/null +++ b/src/app/error/forbidden/index.ts @@ -0,0 +1 @@ +export { ForbiddenComponent } from './forbidden.component'; diff --git a/src/app/error/internal-server-error/index.ts b/src/app/error/internal-server-error/index.ts new file mode 100644 index 0000000..e0d7f38 --- /dev/null +++ b/src/app/error/internal-server-error/index.ts @@ -0,0 +1 @@ +export { InternalServerErrorComponent } from './internal-server-error.component'; diff --git a/src/app/error/internal-server-error/internal-server-error.component.html b/src/app/error/internal-server-error/internal-server-error.component.html new file mode 100644 index 0000000..f2e5626 --- /dev/null +++ b/src/app/error/internal-server-error/internal-server-error.component.html @@ -0,0 +1 @@ +

Internal Server Error

\ No newline at end of file diff --git a/src/app/error/internal-server-error/internal-server-error.component.scss b/src/app/error/internal-server-error/internal-server-error.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/error/internal-server-error/internal-server-error.component.ts b/src/app/error/internal-server-error/internal-server-error.component.ts new file mode 100644 index 0000000..bfc4bcf --- /dev/null +++ b/src/app/error/internal-server-error/internal-server-error.component.ts @@ -0,0 +1,30 @@ +/* + * 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 { Component } from '@angular/core'; + +@Component({ + selector: 're-internal-server-error', + templateUrl: './internal-server-error.component.html', + styleUrls: ['./internal-server-error.component.scss'] +}) +export class InternalServerErrorComponent { } diff --git a/src/app/error/locked/index.ts b/src/app/error/locked/index.ts new file mode 100644 index 0000000..270664e --- /dev/null +++ b/src/app/error/locked/index.ts @@ -0,0 +1 @@ +export { LockedComponent } from './locked.component'; diff --git a/src/app/error/locked/locked.component.html b/src/app/error/locked/locked.component.html new file mode 100644 index 0000000..d3da4a5 --- /dev/null +++ b/src/app/error/locked/locked.component.html @@ -0,0 +1 @@ +

Locked

\ No newline at end of file diff --git a/src/app/error/locked/locked.component.scss b/src/app/error/locked/locked.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/error/locked/locked.component.ts b/src/app/error/locked/locked.component.ts new file mode 100644 index 0000000..af6b62e --- /dev/null +++ b/src/app/error/locked/locked.component.ts @@ -0,0 +1,30 @@ +/* + * 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 { Component } from '@angular/core'; + +@Component({ + selector: 're-locked', + templateUrl: './locked.component.html', + styleUrls: ['./locked.component.scss'] +}) +export class LockedComponent { } diff --git a/src/app/error/not-found/index.ts b/src/app/error/not-found/index.ts new file mode 100644 index 0000000..ed3aaa0 --- /dev/null +++ b/src/app/error/not-found/index.ts @@ -0,0 +1 @@ +export { NotFoundComponent } from './not-found.component'; diff --git a/src/app/error/not-found/not-found.component.html b/src/app/error/not-found/not-found.component.html new file mode 100644 index 0000000..c678e37 --- /dev/null +++ b/src/app/error/not-found/not-found.component.html @@ -0,0 +1 @@ +

Not found

\ No newline at end of file diff --git a/src/app/error/not-found/not-found.component.scss b/src/app/error/not-found/not-found.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/error/not-found/not-found.component.ts b/src/app/error/not-found/not-found.component.ts new file mode 100644 index 0000000..6bcb7ee --- /dev/null +++ b/src/app/error/not-found/not-found.component.ts @@ -0,0 +1,30 @@ +/* + * 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 { Component } from '@angular/core'; + +@Component({ + selector: 're-not-found', + templateUrl: './not-found.component.html', + styleUrls: ['./not-found.component.scss'] +}) +export class NotFoundComponent { } diff --git a/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.html b/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.html index 832ecc6..3d9cd19 100644 --- a/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.html +++ b/src/app/record-editor/json-editor-wrapper/json-editor-wrapper.component.html @@ -18,7 +18,7 @@ - +
{ + this.apiService.lockRecord(); + }); record: object; schema: object; @@ -47,6 +62,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, @@ -56,28 +72,17 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O super(); } - ngOnChanges(changes: SimpleChanges) { - if ((changes['recordId'] || changes['recordType']) && this.recordId && this.recordType) { - // component loaded and being used by record-search - this.record = undefined; // don't display old record while new is loading - this.fetch(this.recordType, this.recordId); - } - } - ngOnInit() { this.domUtilsService.registerBeforeUnloadPrompt(); 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']) - .takeUntil(this.isDestroyed) - .subscribe(params => { - this.fetch(params['type'], params['recid']); - }); - } + this.route.data + .takeUntil(this.isDestroyed) + .filter((data: { resources: RecordResources }) => data.resources != null) + .subscribe((data: { resources: RecordResources }) => { + this.assignResourcesToPropertiesWithSideEffects(data.resources); + }); this.appConfigService.onConfigChange .takeUntil(this.isDestroyed) @@ -87,6 +92,22 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O }); } + private assignResourcesToPropertiesWithSideEffects(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(); + + this.notIdle.start(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.notIdle.stop(); + } + onRecordChange(record: object) { // update record if the edited one is not revision. if (!this.revision) { @@ -100,12 +121,6 @@ export class JsonEditorWrapperComponent extends SubscriberComponent implements O } } - onRevisionRevert() { - this.record = this.revision; - this.revision = undefined; - this.changeDetectorRef.markForCheck(); - } - onRevisionChange(revision: Object) { this.revision = revision; this.changeDetectorRef.markForCheck(); @@ -115,44 +130,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.module.ts b/src/app/record-editor/record-editor.module.ts index 457e01e..abe334a 100644 --- a/src/app/record-editor/record-editor.module.ts +++ b/src/app/record-editor/record-editor.module.ts @@ -31,7 +31,6 @@ import { RecordToolbarComponent } from './record-toolbar'; import { RecordHistoryComponent } from './record-history'; import { SearchBarComponent } from './search-bar'; import { TicketsComponent, NewTicketModalComponent, TicketComponent } from './tickets'; -import { RecordSearchComponent } from './record-search'; import { SavePreviewModalComponent } from './save-preview-modal'; import { ManualMergeModalComponent } from './manual-merge-modal'; @@ -48,7 +47,6 @@ import { ManualMergeModalComponent } from './manual-merge-modal'; TicketsComponent, NewTicketModalComponent, TicketComponent, - RecordSearchComponent, SavePreviewModalComponent, ManualMergeModalComponent ] diff --git a/src/app/record-editor/record-editor.router.ts b/src/app/record-editor/record-editor.router.ts index c32c20c..7c57af4 100644 --- a/src/app/record-editor/record-editor.router.ts +++ b/src/app/record-editor/record-editor.router.ts @@ -1,13 +1,36 @@ import { Routes, RouterModule } from '@angular/router'; 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'; +import { RecordTicketsResolver } from './record-tickets.resolver'; const recordEditorRoutes: Routes = [ - { path: ':type/search', component: RecordSearchComponent }, - { path: ':type/:recid', component: JsonEditorWrapperComponent } + { + path: ':type/search', + resolve: { foundRecordId: RecordSearchResolver }, + runGuardsAndResolvers: 'paramsOrQueryParamsChange', + children: [ + { + path: '', + component: JsonEditorWrapperComponent, + resolve: { + resources: RecordResourcesResolver, + tickets: RecordTicketsResolver + }, + runGuardsAndResolvers: 'paramsOrQueryParamsChange' + } + ] + }, + { + path: ':type/:recid', + component: JsonEditorWrapperComponent, + resolve: { + resources: RecordResourcesResolver, + tickets: RecordTicketsResolver + } + } ]; @NgModule({ @@ -15,7 +38,12 @@ const recordEditorRoutes: Routes = [ RouterModule.forChild(recordEditorRoutes) ], exports: [ - RouterModule, + RouterModule + ], + providers: [ + RecordResourcesResolver, + RecordSearchResolver, + RecordTicketsResolver ] }) export class RecordEditorRouter { } diff --git a/src/app/record-editor/record-history/record-history.component.ts b/src/app/record-editor/record-history/record-history.component.ts index 6baf375..a762b4d 100644 --- a/src/app/record-editor/record-history/record-history.component.ts +++ b/src/app/record-editor/record-history/record-history.component.ts @@ -78,7 +78,7 @@ export class RecordHistoryComponent extends SubscriberComponent implements OnIni private fetchRevisions() { this.apiService .fetchRevisions() - .then(revisions => { + .subscribe(revisions => { this.revisions = revisions; this.changeDetectorRef.markForCheck(); }); 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..6fcf502 --- /dev/null +++ b/src/app/record-editor/record-resources.resolver.ts @@ -0,0 +1,52 @@ +/* + * 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 recordType = route.params.type; + const recordId = route.params.recid || route.parent.data.foundRecordId; + + if (!recordId) { + return Observable.of(null); + } + + return this.apiService + .fetchRecordResources(recordType, recordId) + .take(1) + .catch((error: ApiError) => { + this.router.navigate(['error', error.status]); + return Observable.empty(); + }); + } +} 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..131f425 --- /dev/null +++ b/src/app/record-editor/record-search.resolver.ts @@ -0,0 +1,62 @@ +/* + * 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, 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 apiService: RecordApiService, + private recordSearchService: RecordSearchService) { } + + resolve(route: ActivatedRouteSnapshot): Observable { + const recordType = route.params.type; + const query = route.queryParams.query; + + if (!query) { + return Observable.of(null); + } + + let cursor = Number(route.queryParams.cursor); + + return this.recordSearchService + .search(recordType, query) + .filter(foundIds => foundIds.length > 0) + .do(foundIds => { + if (!cursor || cursor >= foundIds.length || cursor < 0) { + cursor = 0; + } + }) + .map(foundIds => String(foundIds[cursor])) + .catch((error: ApiError) => { + this.router.navigate(['error', error.status]); + return Observable.empty(); + }); + } +} diff --git a/src/app/record-editor/record-search/index.ts b/src/app/record-editor/record-search/index.ts deleted file mode 100644 index e0f6847..0000000 --- a/src/app/record-editor/record-search/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { RecordSearchComponent } from './record-search.component'; diff --git a/src/app/record-editor/record-search/record-search.component.html b/src/app/record-editor/record-search/record-search.component.html deleted file mode 100644 index d9c9064..0000000 --- a/src/app/record-editor/record-search/record-search.component.html +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/src/app/record-editor/record-search/record-search.component.ts b/src/app/record-editor/record-search/record-search.component.ts deleted file mode 100644 index 8490899..0000000 --- a/src/app/record-editor/record-search/record-search.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * This file is part of record-editor. - * Copyright (C) 2017 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 { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -import { Observable } from 'rxjs/Observable'; - -import { RecordSearchService } from '../../core/services'; -import { SearchParams } from '../../shared/interfaces'; -import { SubscriberComponent } from '../../shared/classes'; - -interface RouteType { - params: { type: string }; - queryParams: SearchParams; -} - -@Component({ - selector: 're-record-search', - templateUrl: './record-search.component.html', - styleUrls: ['./record-search.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class RecordSearchComponent extends SubscriberComponent implements OnInit { - - recordType: string; - recordCursor: number; - foundRecordIds: Array; - - constructor(private route: ActivatedRoute, - private changeDetectorRef: ChangeDetectorRef, - private recordSearchService: RecordSearchService) { - super(); - } - - ngOnInit() { - this.recordSearchService.cursor$ - .takeUntil(this.isDestroyed) - .subscribe(cursor => { - this.recordCursor = cursor; - 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)) - .takeUntil(this.isDestroyed) - .subscribe(recordIds => { - this.foundRecordIds = recordIds; - this.changeDetectorRef.markForCheck(); - }); - } - -} diff --git a/src/app/record-editor/record-tickets.resolver.ts b/src/app/record-editor/record-tickets.resolver.ts new file mode 100644 index 0000000..be60ffc --- /dev/null +++ b/src/app/record-editor/record-tickets.resolver.ts @@ -0,0 +1,42 @@ +/* + * 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 } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { RecordApiService } from '../core/services'; +import { Ticket } from '../shared/interfaces'; +import { ApiError } from '../shared/classes'; + +@Injectable() +export class RecordTicketsResolver implements Resolve> { + + constructor(private apiService: RecordApiService) { } + + resolve(route: ActivatedRouteSnapshot): Observable> { + return this.apiService + .fetchRecordTickets() + // TODO: `Observable.throw(error)` when https://github.com/angular/angular/issues/13873 is resolved + .catch((error: ApiError) => Observable.of(null)); + } +} diff --git a/src/app/record-editor/search-bar/search-bar.component.html b/src/app/record-editor/search-bar/search-bar.component.html index cdf0d41..78ccca7 100644 --- a/src/app/record-editor/search-bar/search-bar.component.html +++ b/src/app/record-editor/search-bar/search-bar.component.html @@ -13,9 +13,9 @@ - + {{cursor + 1}} / {{resultCount}} - +
\ No newline at end of file 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..507b618 100644 --- a/src/app/record-editor/search-bar/search-bar.component.ts +++ b/src/app/record-editor/search-bar/search-bar.component.ts @@ -55,13 +55,6 @@ export class SearchBarComponent extends SubscriberComponent implements OnInit { } ngOnInit() { - this.recordSearchService.cursor$ - .takeUntil(this.isDestroyed) - .subscribe(cursor => { - this.cursor = cursor; - this.changeDetectorRef.markForCheck(); - }); - this.recordSearchService.resultCount$ .takeUntil(this.isDestroyed) .subscribe(resultCount => { @@ -73,6 +66,7 @@ export class SearchBarComponent extends SubscriberComponent implements OnInit { .takeUntil(this.isDestroyed) .subscribe((params: SearchParams) => { this.query = params.query; + this.cursor = Number(params.cursor) || 0; this.changeDetectorRef.markForCheck(); }); @@ -113,9 +107,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 } }); } } @@ -145,12 +139,21 @@ export class SearchBarComponent extends SubscriberComponent implements OnInit { private next() { this.cursor++; - this.recordSearchService.setCursor(this.cursor); + this.navigateToCursor(); } private previous() { this.cursor--; - this.recordSearchService.setCursor(this.cursor); + this.navigateToCursor(); + } + + private navigateToCursor() { + this.router.navigate(['record', this.recordType, 'search'], { + queryParams: { + query: this.query, + cursor: this.cursor + } + }); } } diff --git a/src/app/record-editor/tickets/tickets.component.ts b/src/app/record-editor/tickets/tickets.component.ts index 5d07a7f..1b53330 100644 --- a/src/app/record-editor/tickets/tickets.component.ts +++ b/src/app/record-editor/tickets/tickets.component.ts @@ -21,12 +21,10 @@ */ import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; - -import { ToastrService } from 'ngx-toastr'; +import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; -import { RecordApiService } from '../../core/services'; import { Ticket } from '../../shared/interfaces'; import { SubscriberComponent } from '../../shared/classes'; @@ -44,16 +42,19 @@ export class TicketsComponent extends SubscriberComponent implements OnInit { displayLimit = 1; tickets: Array; - constructor(private apiService: RecordApiService, - private toastrService: ToastrService, + constructor(private route: ActivatedRoute, private changeDetectorRef: ChangeDetectorRef) { super(); } ngOnInit() { - this.apiService.newRecordFetched$ + // TODO: handle error after https://github.com/angular/angular/issues/13873 is resolved + this.route.data .takeUntil(this.isDestroyed) - .subscribe(() => this.fetchTickets()); + .subscribe((data: { tickets: Array}) => { + this.tickets = data.tickets; + this.changeDetectorRef.markForCheck(); + }); } onTicketResolve(ticketIndex: number) { @@ -63,18 +64,4 @@ export class TicketsComponent extends SubscriberComponent implements OnInit { onTicketCreate(ticket: Ticket) { this.tickets.push(ticket); } - - private fetchTickets() { - this.apiService.fetchRecordTickets() - .then(tickets => { - this.tickets = tickets; - this.changeDetectorRef.markForCheck(); - }).catch(error => { - if (error.status === 403) { - this.toastrService.error('Logged in user can not access to tickets', 'Forbidden'); - } else { - this.toastrService.error('Could not load the tickets!', 'Error'); - } - }); - } } diff --git a/src/app/shared/classes/api-error.ts b/src/app/shared/classes/api-error.ts index dab7547..dbeac41 100644 --- a/src/app/shared/classes/api-error.ts +++ b/src/app/shared/classes/api-error.ts @@ -31,8 +31,15 @@ export class ApiError { constructor(error: Response, parseBody = true) { this.status = error.status; - this.body = parseBody ? - error.json() : Object.create(null); + if (parseBody) { + try { + this.body = error.json(); + } catch (_) { + this.body = Object.create(null); + } + } else { + this.body = Object.create(null); + } } get message(): string | undefined { 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..c46e12b 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -21,6 +21,7 @@ import 'zone.js/dist/zone'; // common rxjs imports import 'rxjs/add/observable/combineLatest'; +import 'rxjs/add/observable/empty'; import 'rxjs/add/observable/from'; import 'rxjs/add/observable/throw'; @@ -29,5 +30,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';