diff --git a/CHANGELOG.md b/CHANGELOG.md index b525736727..cf35018b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,86 @@ # Changelog +## [11.2.0](https://github.com/dasch-swiss/dsp-das/compare/v11.1.7...v11.2.0) (2024-01-26) + + +### Enhancements + +* create api routes for /admin ([#1318](https://github.com/dasch-swiss/dsp-das/issues/1318)) ([da5f289](https://github.com/dasch-swiss/dsp-das/commit/da5f2894d24be67ec5106696ac11f2848062b0f5)) +* **dsp-app:** introduced ontology class item count from state ([#1358](https://github.com/dasch-swiss/dsp-das/issues/1358)) ([f8984e4](https://github.com/dasch-swiss/dsp-das/commit/f8984e4c2bb488043e71892e9a8e4ac9af315492)) +* **project form:** creating a new project with an existing short code (DEV-3204) ([#1368](https://github.com/dasch-swiss/dsp-das/issues/1368)) ([d231c51](https://github.com/dasch-swiss/dsp-das/commit/d231c519bc3d6e5d6429ea57ae55e9a128513432)) +* **projects:** caches projects loading, removed readProjects from state model. #DEV-2913 ([#1341](https://github.com/dasch-swiss/dsp-das/issues/1341)) ([7ebd126](https://github.com/dasch-swiss/dsp-das/commit/7ebd126e46ea2522351247a764eaa64f20a1a64c)) +* update @dasch-swiss/jdnconvertiblecalendar and @dasch-swiss/jdnconvertiblecalendardateadapter ([#1362](https://github.com/dasch-swiss/dsp-das/issues/1362)) ([cf1cd76](https://github.com/dasch-swiss/dsp-das/commit/cf1cd769740589cd4012d326661ba5c5276031dd)) + + +### Bug Fixes + +* #DEV-3111, #DEV-3108, #DEV-3118 project initialisation from url ([#1316](https://github.com/dasch-swiss/dsp-das/issues/1316)) ([4e5c19a](https://github.com/dasch-swiss/dsp-das/commit/4e5c19a992d00a7593534d72e3e96506b269c86c)) +* add icon to common input ([#1333](https://github.com/dasch-swiss/dsp-das/issues/1333)) ([7d58e69](https://github.com/dasch-swiss/dsp-das/commit/7d58e6987fd4b0fd7cd1a213e2e6cda4817eca76)) +* add min and max length to keywords validators ([#1379](https://github.com/dasch-swiss/dsp-das/issues/1379)) ([2c4852b](https://github.com/dasch-swiss/dsp-das/commit/2c4852b787667a7af56dd6733f9220f7972e7d44)) +* add tooltip on non accessible list ([#1345](https://github.com/dasch-swiss/dsp-das/issues/1345)) ([a96f40b](https://github.com/dasch-swiss/dsp-das/commit/a96f40bbac242efb497900d18364d052f616beb4)) +* adding new value for optional resource #DEV-3031 ([#1360](https://github.com/dasch-swiss/dsp-das/issues/1360)) ([420a564](https://github.com/dasch-swiss/dsp-das/commit/420a564a8371396f70c196a645e7a25d93a4c78d)) +* advanced search number of results bug ([#1388](https://github.com/dasch-swiss/dsp-das/issues/1388)) ([2ad2be0](https://github.com/dasch-swiss/dsp-das/commit/2ad2be011f7c3c0d16ecd78efbd212cff13a3a15)) +* api url loaded from environment ([#1332](https://github.com/dasch-swiss/dsp-das/issues/1332)) ([568f078](https://github.com/dasch-swiss/dsp-das/commit/568f07851d0d837a5ec6c9c93f7f7f4b0296f9bc)) +* audio slider moves on time change ([#1376](https://github.com/dasch-swiss/dsp-das/issues/1376)) ([0abdfc6](https://github.com/dasch-swiss/dsp-das/commit/0abdfc62a00edad7aa3ad1d7b60513b0e78d93ba)) +* cancel button in project form goes back to previous page ([#1385](https://github.com/dasch-swiss/dsp-das/issues/1385)) ([604b2f0](https://github.com/dasch-swiss/dsp-das/commit/604b2f03dd2c78e5fe862d458f504fa10dd602b5)) +* class instance adding and preview. ([#1317](https://github.com/dasch-swiss/dsp-das/issues/1317)) ([3c1e684](https://github.com/dasch-swiss/dsp-das/commit/3c1e684f7b334f35724e13891d89badc81f6a35c)) +* close dialog after update ([#1352](https://github.com/dasch-swiss/dsp-das/issues/1352)) ([41d3bb0](https://github.com/dasch-swiss/dsp-das/commit/41d3bb059ccf22038f3008d00f537c12710200d2)) +* delete failing e2e test ([3dbca58](https://github.com/dasch-swiss/dsp-das/commit/3dbca58a0575e6f44c1f88530b7e4769656f0266)) +* **dsp-app:** handles project edit errors (#DEV-3205) ([#1369](https://github.com/dasch-swiss/dsp-das/issues/1369)) ([27f58b3](https://github.com/dasch-swiss/dsp-das/commit/27f58b3de4c7548c22d549f889e9a48b67412d1d)) +* **dsp-app:** manage user membership (#DEV-3218) ([#1373](https://github.com/dasch-swiss/dsp-das/issues/1373)) ([1c0ba4a](https://github.com/dasch-swiss/dsp-das/commit/1c0ba4afa5449bf4f3a2423c8e485b7db641ee1c)) +* **dsp-app:** new projects have 20 characters shortcode ([#1279](https://github.com/dasch-swiss/dsp-das/issues/1279)) ([cd3be0d](https://github.com/dasch-swiss/dsp-das/commit/cd3be0d7cafe4ba7c8c5783ba37afd877b82e2f9)) +* **dsp-app:** ontology loading, ontology clases refresh, reloads proj… ([#1346](https://github.com/dasch-swiss/dsp-das/issues/1346)) ([9e4fcd3](https://github.com/dasch-swiss/dsp-das/commit/9e4fcd331d739428fea44a017517f464c1d8b24e)) +* **dsp-app:** reloads resource list when resource is changed, separates projects and membership loading.. ([#1343](https://github.com/dasch-swiss/dsp-das/issues/1343)) ([b4bd34b](https://github.com/dasch-swiss/dsp-das/commit/b4bd34b3f3cf7f3eaf5c3089df94d7afba549bb5)) +* **dsp-app:** Resolves DEV-2687 ([#1336](https://github.com/dasch-swiss/dsp-das/issues/1336)) ([d6211f9](https://github.com/dasch-swiss/dsp-das/commit/d6211f91169d25e1edcbeefc1251b64acfb36616)) +* **dsp-app:** resource class form validation (#DEV-3132) ([#1370](https://github.com/dasch-swiss/dsp-das/issues/1370)) ([94e7a48](https://github.com/dasch-swiss/dsp-das/commit/94e7a482904f31347fc47b72a646eaddd67b3ed4)) +* **dsp-app:** resource link form (#DEV-3203) ([#1377](https://github.com/dasch-swiss/dsp-das/issues/1377)) ([a0f9afb](https://github.com/dasch-swiss/dsp-das/commit/a0f9afbbc3313c5ae2c9558a9fb7b31057b9ef6b)) +* fixed loading coumpound resources #DEV-3096 ([#1326](https://github.com/dasch-swiss/dsp-das/issues/1326)) ([490513e](https://github.com/dasch-swiss/dsp-das/commit/490513ee35a97db4c3446a01bedd694247de261f)) +* flickering button ([#1371](https://github.com/dasch-swiss/dsp-das/issues/1371)) ([ec0a850](https://github.com/dasch-swiss/dsp-das/commit/ec0a8500327d923c213d3426b086c65767f9bbc8)) +* geoname property lookup ([#1334](https://github.com/dasch-swiss/dsp-das/issues/1334)) ([a491062](https://github.com/dasch-swiss/dsp-das/commit/a491062ea97e617653a5712a555f0724ccfd1f95)) +* keywords chip inputs cannot be duplicated ([#1382](https://github.com/dasch-swiss/dsp-das/issues/1382)) ([ac5298a](https://github.com/dasch-swiss/dsp-das/commit/ac5298af507e6f4c157288adc775c9e1705d11b4)) +* links to incunabula project ([#1311](https://github.com/dasch-swiss/dsp-das/issues/1311)) ([31a6458](https://github.com/dasch-swiss/dsp-das/commit/31a6458477e1167a0d8e2be9f64c889dff47b7f7)) +* list item comment get updated ([#1386](https://github.com/dasch-swiss/dsp-das/issues/1386)) ([1bb85a7](https://github.com/dasch-swiss/dsp-das/commit/1bb85a7887c61a515eb3fd9b37852d1896f35af5)) +* **list-view:** correct display logic ([#1378](https://github.com/dasch-swiss/dsp-das/issues/1378)) ([b68f8db](https://github.com/dasch-swiss/dsp-das/commit/b68f8dbf64e4f15afdf5e5ba3fcaa18015054b61)) +* loaders, refactored project component getters ([#1361](https://github.com/dasch-swiss/dsp-das/issues/1361)) ([f87c96b](https://github.com/dasch-swiss/dsp-das/commit/f87c96ba8a19e682af44b0723addcbf722ed3ed3)) +* merge conflict ([#1308](https://github.com/dasch-swiss/dsp-das/issues/1308)) ([818dcd6](https://github.com/dasch-swiss/dsp-das/commit/818dcd6049171d4c0ff956bb3ff9f88b3bee54ff)) +* more readable error message ([#1383](https://github.com/dasch-swiss/dsp-das/issues/1383)) ([c426d92](https://github.com/dasch-swiss/dsp-das/commit/c426d923f9fc88589930076976d72f4bbcda6ab1)) +* multi language forms first selection bug ([#1344](https://github.com/dasch-swiss/dsp-das/issues/1344)) ([47a161c](https://github.com/dasch-swiss/dsp-das/commit/47a161c509a51a647f62504c0bba59ca5781fbd9)) +* multi-language form default language ([#1353](https://github.com/dasch-swiss/dsp-das/issues/1353)) ([e5fb2fe](https://github.com/dasch-swiss/dsp-das/commit/e5fb2fe1bc40d0d93040c1de6ebe2fddd6a304ee)) +* no retry in edit user form ([#1367](https://github.com/dasch-swiss/dsp-das/issues/1367)) ([1f03e7d](https://github.com/dasch-swiss/dsp-das/commit/1f03e7d2a3c1886a57d07f670dd8bb524b3ba858)) +* ontology resource create and display ([#1322](https://github.com/dasch-swiss/dsp-das/issues/1322)) ([adfc304](https://github.com/dasch-swiss/dsp-das/commit/adfc3042ccc83293b77e6aad602dc0c31fd8b2bf)) +* Project initialisation from url ([#1306](https://github.com/dasch-swiss/dsp-das/issues/1306)) ([6d8e1a3](https://github.com/dasch-swiss/dsp-das/commit/6d8e1a36e973489c5a25402e1cd3c4034ab982e3)) +* Project Member cannot add instances of resources #DEV-3121 ([#1327](https://github.com/dasch-swiss/dsp-das/issues/1327)) ([e0f57fe](https://github.com/dasch-swiss/dsp-das/commit/e0f57fe33db9c717e15404abe8576e94e4eb5e83)) +* **projects list:** deactivating-a-project-not-possible (DEV-3206) ([#1366](https://github.com/dasch-swiss/dsp-das/issues/1366)) ([ad76437](https://github.com/dasch-swiss/dsp-das/commit/ad76437e915e7400853d600df27187c125262015)) +* property value refresh #DEV-3106 ([#1329](https://github.com/dasch-swiss/dsp-das/issues/1329)) ([f41b375](https://github.com/dasch-swiss/dsp-das/commit/f41b375de5ec28dbc637c5186cd7fd4c99a39ad6)) +* redirection to home issue when session is not valid ([#1359](https://github.com/dasch-swiss/dsp-das/issues/1359)) ([45e0625](https://github.com/dasch-swiss/dsp-das/commit/45e0625ee24db55a51ed22b1fba9d8e36f61b4c0)) +* remove unused validator on project form ([#1339](https://github.com/dasch-swiss/dsp-das/issues/1339)) ([0b9ad0c](https://github.com/dasch-swiss/dsp-das/commit/0b9ad0c762142835dae8a790dab185811310c57c)) +* removeFromProjectMembership API http method, project membership … ([#1330](https://github.com/dasch-swiss/dsp-das/issues/1330)) ([c1ab00c](https://github.com/dasch-swiss/dsp-das/commit/c1ab00ca5b16bcd3b0ee3e6f4bf39b112a002fa1)) +* send gravsearch and search-count request simultaniously ([#1380](https://github.com/dasch-swiss/dsp-das/issues/1380)) ([89b91ec](https://github.com/dasch-swiss/dsp-das/commit/89b91ecb7a28fc46be5cd879b9204489f926d597)) +* shortcode issue gravsearch ([#1315](https://github.com/dasch-swiss/dsp-das/issues/1315)) ([814fb98](https://github.com/dasch-swiss/dsp-das/commit/814fb9839d76f9309f92f7e5c5122a8fe94ee962)) +* shortcode issue in gravsearch ([#1314](https://github.com/dasch-swiss/dsp-das/issues/1314)) ([1b69de4](https://github.com/dasch-swiss/dsp-das/commit/1b69de4832142aaff0209baddb65d288731daf22)) +* **still image:** User without permission can draw a region ([#1372](https://github.com/dasch-swiss/dsp-das/issues/1372)) ([7da5827](https://github.com/dasch-swiss/dsp-das/commit/7da582775c2d4e12908b0f2f5430853312801ed2)) +* various bugs ([#1312](https://github.com/dasch-swiss/dsp-das/issues/1312)) ([9f55ce1](https://github.com/dasch-swiss/dsp-das/commit/9f55ce15a8c12929e22041a46dbaaa6a2d4538f0)) +* various bugs ([#1313](https://github.com/dasch-swiss/dsp-das/issues/1313)) ([1d90cf7](https://github.com/dasch-swiss/dsp-das/commit/1d90cf7aea3f99a71ebf1a54a50b515b2e0174c7)) + + +### Maintenance + +* add confirm dialog service ([#1335](https://github.com/dasch-swiss/dsp-das/issues/1335)) ([3bc2b6c](https://github.com/dasch-swiss/dsp-das/commit/3bc2b6c6e9664e4a3b88f62ea52fd7b1526978b1)) +* add prettier basic configuration ([#1309](https://github.com/dasch-swiss/dsp-das/issues/1309)) ([99276a6](https://github.com/dasch-swiss/dsp-das/commit/99276a64c3247af424a0cadfea020c2ac0c3de8a)) +* add prettier general style ([#1310](https://github.com/dasch-swiss/dsp-das/issues/1310)) ([8567eda](https://github.com/dasch-swiss/dsp-das/commit/8567eda20202d58ab402fa3ab699b37b05214fa9)) +* add recommended linting rules again ([#1323](https://github.com/dasch-swiss/dsp-das/issues/1323)) ([2fb7c3f](https://github.com/dasch-swiss/dsp-das/commit/2fb7c3f09ec4f5ac0b36b99215a01fe13dfc0173)) +* add rule nx enforce-module-boundaries ([#1325](https://github.com/dasch-swiss/dsp-das/issues/1325)) ([1b1fd77](https://github.com/dasch-swiss/dsp-das/commit/1b1fd77abfb5bade3f738d8eff218fd64cf9ee00)) +* confirm dialog, multi language input, project-form, resource-form, list-info-form ([#1328](https://github.com/dasch-swiss/dsp-das/issues/1328)) ([4c520cd](https://github.com/dasch-swiss/dsp-das/commit/4c520cd4ba33d9eaf77dc5ec648b7e8e4970c215)) +* **dsp-api:** created api-entity-helper ([#1337](https://github.com/dasch-swiss/dsp-das/issues/1337)) ([3befc8e](https://github.com/dasch-swiss/dsp-das/commit/3befc8efc5c1472c6ad655958407a504d33bd6ce)) +* **linter:** use standard angular linter ([#1276](https://github.com/dasch-swiss/dsp-das/issues/1276)) ([e7251d1](https://github.com/dasch-swiss/dsp-das/commit/e7251d1e632a7bf4560faaec0897a3f350706ced)) +* make list forms files clearer ([#1338](https://github.com/dasch-swiss/dsp-das/issues/1338)) ([c5ac901](https://github.com/dasch-swiss/dsp-das/commit/c5ac901710fd4cf0e2ed7adac3b319c18744e506)) +* reinforce linter ([#1320](https://github.com/dasch-swiss/dsp-das/issues/1320)) ([5e60532](https://github.com/dasch-swiss/dsp-das/commit/5e605321b149468767ad4aa8c35e023e1f4d9f18)) +* remove dateAdapter app ([#1324](https://github.com/dasch-swiss/dsp-das/issues/1324)) ([28e8ee4](https://github.com/dasch-swiss/dsp-das/commit/28e8ee4f6c48d7e86cecf6e052467fe53b6f9fc6)) +* remove unused external-link-directive ([#1340](https://github.com/dasch-swiss/dsp-das/issues/1340)) ([bf9aecc](https://github.com/dasch-swiss/dsp-das/commit/bf9aecc697b38f258921e52eea8bd1a53d1fa957)) +* run prettier linter on apps and libs folders ([#1319](https://github.com/dasch-swiss/dsp-das/issues/1319)) ([786753d](https://github.com/dasch-swiss/dsp-das/commit/786753dc4200f0ffc7073fa3bf36169cf9ce6199)) +* set max line length to 120 characters ([#1321](https://github.com/dasch-swiss/dsp-das/issues/1321)) ([fa379d0](https://github.com/dasch-swiss/dsp-das/commit/fa379d0ea99f8c44787791947c8a67c56cfc449e)) +* Update dsp-js to v9.1.10 ([#1389](https://github.com/dasch-swiss/dsp-das/issues/1389)) ([f1489d6](https://github.com/dasch-swiss/dsp-das/commit/f1489d6acf1941f51f343c000c702a53b7d9089b)) + ## [11.1.7](https://github.com/dasch-swiss/dsp-das/compare/v11.1.6...v11.1.7) (2023-12-08) diff --git a/apps/dsp-app/src/app/app.component.ts b/apps/dsp-app/src/app/app.component.ts index f15b157b5f..1de752fdc8 100644 --- a/apps/dsp-app/src/app/app.component.ts +++ b/apps/dsp-app/src/app/app.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; +import { AutoLoginService, LocalStorageWatcherService } from '@dasch-swiss/vre/shared/app-session'; @Component({ selector: 'app-root', @@ -14,9 +15,12 @@ export class AppComponent implements OnInit { constructor( private _router: Router, - private _titleService: Title + private _titleService: Title, + private _autoLoginService: AutoLoginService, + private _localStorageWatcher: LocalStorageWatcherService ) { - // set the page title + this._autoLoginService.setup(); + this._localStorageWatcher.watchAccessToken(); this._titleService.setTitle('DaSCH Service Platform'); } diff --git a/apps/dsp-app/src/app/app.module.ts b/apps/dsp-app/src/app/app.module.ts index 61f6dc5972..3f78b8f6ce 100644 --- a/apps/dsp-app/src/app/app.module.ts +++ b/apps/dsp-app/src/app/app.module.ts @@ -20,6 +20,7 @@ import { import { AppDatePickerComponent } from '@dasch-swiss/vre/shared/app-date-picker'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { AppLoggingService } from '@dasch-swiss/vre/shared/app-logging'; +import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { AppProgressIndicatorComponent, CenteredLayoutComponent, @@ -34,7 +35,6 @@ import { } from '@dasch-swiss/vre/shared/app-string-literal'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; -import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { AngularSplitModule } from 'angular-split'; import { PdfViewerModule } from 'ng2-pdf-viewer'; import { ColorPickerModule } from 'ngx-color-picker'; @@ -354,7 +354,6 @@ export function httpLoaderFactory(httpClient: HttpClient) { MultiLanguageTextareaComponent, MutiLanguageInputComponent, NgxsStoreModule, - NgxsStoragePluginModule.forRoot(), ], providers: [ AppConfigService, @@ -385,7 +384,7 @@ export function httpLoaderFactory(httpClient: HttpClient) { { provide: ErrorHandler, useClass: AppErrorHandler, - deps: [AppLoggingService], + deps: [NotificationService], }, { provide: HTTP_INTERCEPTORS, diff --git a/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts b/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts index 6315d39db2..abd70b89e7 100644 --- a/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts +++ b/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts @@ -1,10 +1,9 @@ -import { DOCUMENT, Location } from '@angular/common'; +import { Location } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, - Inject, Input, OnDestroy, OnInit, @@ -13,15 +12,8 @@ import { import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { AuthError, AuthService } from '@dasch-swiss/vre/shared/app-session'; -import { LoadUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, Store, ofActionSuccessful } from '@ngxs/store'; -import { Observable, Subject, combineLatest } from 'rxjs'; -import { take, takeLast } from 'rxjs/operators'; -import { - ComponentCommunicationEventService, - EmitEvent, - Events, -} from '../../services/component-communication-event.service'; +import { Subject } from 'rxjs'; +import { takeLast } from 'rxjs/operators'; @Component({ selector: 'app-login-form', @@ -30,9 +22,6 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class LoginFormComponent implements OnInit, OnDestroy { - get isLoggedIn$(): Observable { - return this._authService.isSessionValid$(); - } /** * set whether or not you want icons to display in the input fields * @@ -98,16 +87,12 @@ export class LoginFormComponent implements OnInit, OnDestroy { private destroyed$ = new Subject(); constructor( - private _componentCommsService: ComponentCommunicationEventService, private _fb: UntypedFormBuilder, private router: Router, private _authService: AuthService, private route: ActivatedRoute, private location: Location, - private cd: ChangeDetectorRef, - private _actions$: Actions, - private _store: Store, - @Inject(DOCUMENT) private document: Document + private cd: ChangeDetectorRef ) {} /** @@ -144,33 +129,21 @@ export class LoginFormComponent implements OnInit, OnDestroy { const password: string = this.form.get('password').value; this._authService - .apiLogin$(identifier, password) + .login$(identifier, password) .pipe(takeLast(1)) .subscribe({ next: loginResult => { - if (loginResult) { - this._componentCommsService.emit(new EmitEvent(Events.loginSuccess, true)); - this._store.dispatch(new LoadUserAction(identifier)); - return combineLatest([ - this._actions$.pipe(ofActionSuccessful(LoadUserAction)), - this._store.select(UserSelectors.user), - ]) - .pipe(take(1)) - .subscribe(([action, user]) => { - this.loading = false; - this._authService.loginSuccessfulEvent.emit(user); - this.cd.markForCheck(); - if (this.returnUrl) { - this.router.navigate([this.returnUrl]); - } - }); + this.loginSuccess.emit(true); + this.loading = false; + this.cd.markForCheck(); + + if (this.returnUrl) { + this.router.navigate([this.returnUrl]); } }, error: (error: AuthError) => { this.loginSuccess.emit(false); - this._componentCommsService.emit(new EmitEvent(Events.loginSuccess, false)); - this.loading = false; this.isError = true; diff --git a/apps/dsp-app/src/app/main/directive/existing-name/existing-names.validator.ts b/apps/dsp-app/src/app/main/directive/existing-name/existing-names.validator.ts index 6ce5a8ecad..42695d8d72 100644 --- a/apps/dsp-app/src/app/main/directive/existing-name/existing-names.validator.ts +++ b/apps/dsp-app/src/app/main/directive/existing-name/existing-names.validator.ts @@ -7,15 +7,15 @@ import { AbstractControl, ValidatorFn } from '@angular/forms'; * @param {RegExp} valArrayRegexp List of regular expression values * @returns ValidatorFn */ -export function existingNamesValidator(valArrayRegexp: [RegExp]): ValidatorFn { +export function existingNamesValidator(valArrayRegexp: [RegExp], isCaseSensitive = false): ValidatorFn { return (control: AbstractControl): { [key: string]: any } => { - let name; + let name: string; if (control.value) { - name = control.value.toLowerCase(); + name = isCaseSensitive ? control.value : control.value.toLowerCase(); } - let no; + let no: boolean; for (const existing of valArrayRegexp) { no = existing.test(name); if (no) { diff --git a/apps/dsp-app/src/app/main/guard/auth.guard.ts b/apps/dsp-app/src/app/main/guard/auth.guard.ts index ae86b1adfa..9b766d7532 100644 --- a/apps/dsp-app/src/app/main/guard/auth.guard.ts +++ b/apps/dsp-app/src/app/main/guard/auth.guard.ts @@ -1,45 +1,30 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; -import { ReadUser } from '@dasch-swiss/dsp-js'; import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; -import { AuthService } from '@dasch-swiss/vre/shared/app-session'; -import { SetUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, Select, Store, ofActionCompleted } from '@ngxs/store'; -import { Observable, of } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { AutoLoginService } from '@dasch-swiss/vre/shared/app-session'; +import { UserSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { Store } from '@ngxs/store'; +import { Observable } from 'rxjs'; +import { filter, switchMap, tap } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate { - @Select(UserSelectors.user) user$: Observable; - constructor( - private store: Store, - private _authService: AuthService, - private actions$: Actions, - @Inject(DOCUMENT) private document: Document + private _store: Store, + @Inject(DOCUMENT) private document: Document, + private _autoLoginService: AutoLoginService ) {} canActivate(): Observable { - return this.user$.pipe( - switchMap(user => { - if (user) return of(null); - - if (this.store.selectSnapshot(UserSelectors.isLoading)) { - return this.actions$.pipe(ofActionCompleted(SetUserAction)); - } else { - return this.store.dispatch(new SetUserAction(user)); - } - }), - switchMap(() => this._authService.isSessionValid$(true)), - map(isLoggedIn => { - if (isLoggedIn) { - return true; - } else { + return this._autoLoginService.hasCheckedCredentials$.pipe( + filter(hasChecked => hasChecked === true), + switchMap(() => this._store.select(UserSelectors.isLoggedIn)), + tap(isLoggedIn => { + if (!isLoggedIn) { this._goToHomePage(); - return false; } }) ); diff --git a/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts b/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts index e3fe0a6afe..4b991248c1 100644 --- a/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts +++ b/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts @@ -3,10 +3,9 @@ import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; import { StoredProject } from '@dasch-swiss/dsp-js'; import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; -import { AuthService } from '@dasch-swiss/vre/shared/app-session'; import { UserSelectors } from '@dasch-swiss/vre/shared/app-state'; -import { Select } from '@ngxs/store'; -import { Observable, combineLatest } from 'rxjs'; +import { Select, Store } from '@ngxs/store'; +import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable({ @@ -17,19 +16,19 @@ export class OntologyClassInstanceGuard implements CanActivate { @Select(UserSelectors.userProjects) userProjects$: Observable; constructor( - private authService: AuthService, + private _store: Store, private projectService: ProjectService, private router: Router ) {} canActivate(activatedRoute: ActivatedRouteSnapshot): Observable { const instanceId = activatedRoute.params[RouteConstants.instanceParameter]; - return combineLatest([this.authService.isSessionValid$(), this.isSysAdmin$, this.userProjects$]).pipe( - map(([isSessionValid, isSysAdmin, userProjects]) => { + return combineLatest([this._store.select(UserSelectors.isLoggedIn), this.isSysAdmin$, this.userProjects$]).pipe( + map(([isLoggedIn, isSysAdmin, userProjects]) => { const projectUuid = activatedRoute.parent.params[RouteConstants.uuidParameter]; const isAddInstance = instanceId === RouteConstants.addClassInstance; - if (!isSessionValid && isAddInstance) { + if (!isLoggedIn && isAddInstance) { this.router.navigateByUrl(`/${RouteConstants.project}/${projectUuid}`); return false; } diff --git a/apps/dsp-app/src/app/main/header/header.component.ts b/apps/dsp-app/src/app/main/header/header.component.ts index 5d5fa23c1b..8a2bd43c6d 100644 --- a/apps/dsp-app/src/app/main/header/header.component.ts +++ b/apps/dsp-app/src/app/main/header/header.component.ts @@ -1,21 +1,19 @@ -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { Router } from '@angular/router'; -import { AppConfigService, RouteConstants, DspConfig } from '@dasch-swiss/vre/shared/app-config'; -import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; +import { AppConfigService, DspConfig, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { Subscription } from 'rxjs'; import { SearchParams } from '../../workspace/results/list-view/list-view.component'; import { DialogComponent } from '../dialog/dialog.component'; -import { ComponentCommunicationEventService, Events } from '../services/component-communication-event.service'; @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.scss'], }) -export class HeaderComponent implements OnInit, OnDestroy { +export class HeaderComponent implements OnDestroy { session = false; show = false; searchParams: SearchParams; @@ -29,11 +27,9 @@ export class HeaderComponent implements OnInit, OnDestroy { constructor( private _appConfigService: AppConfigService, - private _componentCommsService: ComponentCommunicationEventService, private _dialog: MatDialog, private _domSanitizer: DomSanitizer, private _matIconRegistry: MatIconRegistry, - private _notification: NotificationService, private _router: Router ) { // create own logo icon to use them in mat-icons @@ -45,12 +41,6 @@ export class HeaderComponent implements OnInit, OnDestroy { this.dsp = this._appConfigService.dspConfig; } - ngOnInit() { - this.componentCommsSubscription = this._componentCommsService.on(Events.loginSuccess, () => { - this._notification.openSnackBar('Login successful'); - }); - } - ngOnDestroy() { // unsubscribe from the ValueOperationEventService when component is destroyed if (this.componentCommsSubscription !== undefined) { diff --git a/apps/dsp-app/src/app/main/help/help.component.ts b/apps/dsp-app/src/app/main/help/help.component.ts index 73f55a48f3..201074d680 100644 --- a/apps/dsp-app/src/app/main/help/help.component.ts +++ b/apps/dsp-app/src/app/main/help/help.component.ts @@ -1,13 +1,6 @@ import { Component, Inject, OnInit } from '@angular/core'; -import { - ApiResponseData, - ApiResponseError, - HealthResponse, - KnoraApiConnection, - VersionResponse, -} from '@dasch-swiss/dsp-js'; +import { ApiResponseData, HealthResponse, KnoraApiConnection, VersionResponse } from '@dasch-swiss/dsp-js'; import { AppConfigService, DspApiConnectionToken, DspConfig } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { environment } from '../../../environments/environment'; import { GridItem } from '../grid/grid.component'; @@ -95,8 +88,7 @@ export class HelpComponent implements OnInit { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _appConfigService: AppConfigService, - private _errorHandler: AppErrorHandler + private _appConfigService: AppConfigService ) {} ngOnInit() { @@ -106,8 +98,9 @@ export class HelpComponent implements OnInit { this.releaseNotesUrl = `https://github.com/dasch-swiss/dsp-das/releases/tag/v${this.appVersion}`; - this._dspApiConnection.system.versionEndpoint.getVersion().subscribe( - (response: ApiResponseData) => { + this._dspApiConnection.system.versionEndpoint + .getVersion() + .subscribe((response: ApiResponseData) => { this.apiVersion = response.body; // set dsp-app version @@ -121,10 +114,6 @@ export class HelpComponent implements OnInit { // set dsp-sipi version this.tools[2].title += this.apiVersion.sipi; this.tools[2].url += this.apiVersion.sipi; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + }); } } diff --git a/apps/dsp-app/src/app/main/http-interceptors/auth-interceptor.ts b/apps/dsp-app/src/app/main/http-interceptors/auth-interceptor.ts index e2900498e9..e364306a39 100644 --- a/apps/dsp-app/src/app/main/http-interceptors/auth-interceptor.ts +++ b/apps/dsp-app/src/app/main/http-interceptors/auth-interceptor.ts @@ -1,13 +1,13 @@ import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { AppConfigService } from '@dasch-swiss/vre/shared/app-config'; -import { AuthService } from '@dasch-swiss/vre/shared/app-session'; +import { AccessTokenService } from '@dasch-swiss/vre/shared/app-session'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor( - private _authService: AuthService, - private _appConfigService: AppConfigService + private _appConfigService: AppConfigService, + private _accessTokenService: AccessTokenService ) {} intercept(req: HttpRequest, next: HttpHandler) { @@ -15,11 +15,11 @@ export class AuthInterceptor implements HttpInterceptor { return next.handle(req); } - const authToken = this._authService.getAccessToken(); + const authToken = this._accessTokenService.getAccessToken(); if (!authToken) return next.handle(req); const authReq = req.clone({ - headers: req.headers.set('Authorization', `Bearer ${this._authService.getAccessToken()}`), + headers: req.headers.set('Authorization', `Bearer ${this._accessTokenService.getAccessToken()}`), }); return next.handle(authReq); } diff --git a/apps/dsp-app/src/app/project/chip-list-input/chip-list-input.component.ts b/apps/dsp-app/src/app/project/chip-list-input/chip-list-input.component.ts index 80c92a2881..cfe4321902 100644 --- a/apps/dsp-app/src/app/project/chip-list-input/chip-list-input.component.ts +++ b/apps/dsp-app/src/app/project/chip-list-input/chip-list-input.component.ts @@ -1,6 +1,6 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { Component, Input } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; +import { FormArray, FormBuilder, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; @Component({ @@ -8,9 +8,11 @@ import { MatChipInputEvent } from '@angular/material/chips'; template: ` - + {{ tag }} - cancel + cancel - {{ errors | humanReadableError }} + {{ errors | humanReadableError }} + New value: {{ addChipFormError | humanReadableError }} `, }) export class ChipListInputComponent { @Input() formGroup: FormGroup; @Input() controlName: string; - @Input() keywords: string[]; - @Input() editable = true; @Input() chipsRequired = true; + @Input() validators: ValidatorFn[]; separatorKeyCodes = [ENTER, COMMA]; + addChipFormError: ValidationErrors | null = null; - get formControl() { - return this.formGroup.controls[this.controlName] as FormControl; - } + constructor(private _fb: FormBuilder) {} - update() { - this.formControl.setValue(this.keywords); - this.formControl.markAsTouched(); + get formArray() { + return this.formGroup.controls[this.controlName] as FormArray; } addKeyword(event: MatChipInputEvent): void { + this.addChipFormError = null; const input = event.chipInput.inputElement; const value = event.value; - if (!this.keywords) { - this.keywords = []; - } - // add keyword - if ((value || '').trim()) { - this.keywords.push(value.trim()); - } + const newValue = (value || '').trim(); + if (!newValue) return; - // reset the input value - if (input) { + if (this.formArray.value.includes(newValue)) { input.value = ''; + return; } - this.update(); - } + const newFormControl = this._fb.control(value, this.validators); - removeKeyword(keyword: any): void { - const index = this.keywords.indexOf(keyword); - - if (index >= 0) { - this.keywords.splice(index, 1); + if (newFormControl.valid) { + this.formArray.push(newFormControl); + input.value = ''; + } else { + this.addChipFormError = newFormControl.errors; } - this.update(); + } + + removeKeyword(index: number): void { + this.formArray.removeAt(index); } trackByFn = (index: number, item: string) => `${index}-${item}`; diff --git a/apps/dsp-app/src/app/project/collaboration/add-user/add-user.component.ts b/apps/dsp-app/src/app/project/collaboration/add-user/add-user.component.ts index e34af3ad3e..322570a0c0 100644 --- a/apps/dsp-app/src/app/project/collaboration/add-user/add-user.component.ts +++ b/apps/dsp-app/src/app/project/collaboration/add-user/add-user.component.ts @@ -19,7 +19,6 @@ import { UsersResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { ProjectsSelectors } from '@dasch-swiss/vre/shared/app-state'; import { Store } from '@ngxs/store'; @@ -121,7 +120,6 @@ export class AddUserComponent implements OnInit { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _formBuilder: UntypedFormBuilder, private _projectService: ProjectService, private _store: Store, @@ -139,64 +137,59 @@ export class AddUserComponent implements OnInit { this.users = []; // get all users - this._dspApiConnection.admin.usersEndpoint.getUsers().subscribe( - (response: ApiResponseData) => { - // if a user is already member of the team, mark it in the list - const members: string[] = []; - - // get all members of this project - const projectMembers = this._store.selectSnapshot(ProjectsSelectors.projectMembers); - if (projectMembers[this.projectIri]) { - for (const m of projectMembers[this.projectIri].value) { - members.push(m.id); - - // if the user is already member of the project - // add the email to the list of existing - this.existingEmailInProject.push(new RegExp(`(?:^|W)${m.email.toLowerCase()}(?:$|W)`)); - // add username to the list of existing - this.existingUsernameInProject.push(new RegExp(`(?:^|W)${m.username.toLowerCase()}(?:$|W)`)); - } - } + this._dspApiConnection.admin.usersEndpoint.getUsers().subscribe((response: ApiResponseData) => { + // if a user is already member of the team, mark it in the list + const members: string[] = []; + + // get all members of this project + const projectMembers = this._store.selectSnapshot(ProjectsSelectors.projectMembers); + if (projectMembers[this.projectIri]) { + for (const m of projectMembers[this.projectIri].value) { + members.push(m.id); - let i = 0; - for (const u of response.body.users) { // if the user is already member of the project // add the email to the list of existing - this.existingEmails.push(new RegExp(`(?:^|W)${u.email.toLowerCase()}(?:$|W)`)); + this.existingEmailInProject.push(new RegExp(`(?:^|W)${m.email.toLowerCase()}(?:$|W)`)); // add username to the list of existing - this.existingUsernames.push(new RegExp(`(?:^|W)${u.username.toLowerCase()}(?:$|W)`)); + this.existingUsernameInProject.push(new RegExp(`(?:^|W)${m.username.toLowerCase()}(?:$|W)`)); + } + } - let existsInProject = ''; + let i = 0; + for (const u of response.body.users) { + // if the user is already member of the project + // add the email to the list of existing + this.existingEmails.push(new RegExp(`(?:^|W)${u.email.toLowerCase()}(?:$|W)`)); + // add username to the list of existing + this.existingUsernames.push(new RegExp(`(?:^|W)${u.username.toLowerCase()}(?:$|W)`)); - if (members && members.indexOf(u.id) > -1) { - existsInProject = '* '; - } + let existsInProject = ''; - this.users[i] = { - iri: u.id, - name: u.username, - label: `${existsInProject + u.username} | ${u.email} | ${u.givenName} ${u.familyName}`, - }; - i++; + if (members && members.indexOf(u.id) > -1) { + existsInProject = '* '; } - this.users.sort((u1: AutocompleteItem, u2: AutocompleteItem) => { - if (u1.label < u2.label) { - return -1; - } else if (u1.label > u2.label) { - return 1; - } else { - return 0; - } - }); - - this.loading = false; - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + this.users[i] = { + iri: u.id, + name: u.username, + label: `${existsInProject + u.username} | ${u.email} | ${u.givenName} ${u.familyName}`, + }; + i++; } - ); + + this.users.sort((u1: AutocompleteItem, u2: AutocompleteItem) => { + if (u1.label < u2.label) { + return -1; + } else if (u1.label > u2.label) { + return 1; + } else { + return 0; + } + }); + + this.loading = false; + this._cd.markForCheck(); + }); this.selectUserForm = this._formBuilder.group({ username: new UntypedFormControl( @@ -281,19 +274,14 @@ export class AddUserComponent implements OnInit { // add user to project this._dspApiConnection.admin.usersEndpoint .addUserToProjectMembership(this.selectedUser.id, this.projectIri) - .subscribe( - () => { - // successful post - // reload the component - this.buildForm(); - this.refreshParent.emit(); - - this.loading = false; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe(() => { + // successful post + // reload the component + this.buildForm(); + this.refreshParent.emit(); + + this.loading = false; + }); } }, (error: ApiResponseError) => { @@ -303,9 +291,6 @@ export class AddUserComponent implements OnInit { this.selectedUser = new ReadUser(); this.selectedUser.email = val; - } else { - // api error - this._errorHandler.showMessage(error); } } ); diff --git a/apps/dsp-app/src/app/project/create-project-form-page/create-project-form-page.component.ts b/apps/dsp-app/src/app/project/create-project-form-page/create-project-form-page.component.ts index 641f3463d0..0c8f34ba31 100644 --- a/apps/dsp-app/src/app/project/create-project-form-page/create-project-form-page.component.ts +++ b/apps/dsp-app/src/app/project/create-project-form-page/create-project-form-page.component.ts @@ -1,3 +1,4 @@ +import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; @@ -5,8 +6,7 @@ import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { LoadProjectsAction } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, Store } from '@ngxs/store'; -import { take } from 'rxjs/operators'; +import { Store } from '@ngxs/store'; @Component({ selector: 'app-create-project-form-page', @@ -23,7 +23,7 @@ import { take } from 'rxjs/operators'; (formValueChange)="form = $event">
- @@ -50,7 +50,7 @@ export class CreateProjectFormPageComponent { private _projectApiService: ProjectApiService, private _store: Store, private _router: Router, - private _actions: Actions + private _location: Location ) {} submitForm() { @@ -65,14 +65,15 @@ export class CreateProjectFormPageComponent { selfjoin: true, status: true, }) - .pipe(take(1)) .subscribe(projectResponse => { const uuid = ProjectService.IriToUuid(projectResponse.project.id); this._store.dispatch(new LoadProjectsAction()); this.loading = false; - this._router - .navigateByUrl(`${RouteConstants.projectRelative}`, { skipLocationChange: true }) - .then(() => this._router.navigate([`${RouteConstants.projectRelative}/${uuid}`])); + this._router.navigate([RouteConstants.projectRelative, uuid]); }); } + + goBack() { + this._location.back(); + } } diff --git a/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item-dialog.component.ts b/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item-dialog.component.ts index 8a7711dc95..58750e8549 100644 --- a/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item-dialog.component.ts +++ b/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item-dialog.component.ts @@ -56,7 +56,7 @@ export class EditListItemDialogComponent { projectIri: this.data.projectIri, listIri: this.data.nodeIri, labels: this.form.value.labels, - comments: this.form.value.descriptions, + comments: this.form.value.comments, }; this._listApiService diff --git a/apps/dsp-app/src/app/project/list/list-item-form/reusable-list-item-form.component.ts b/apps/dsp-app/src/app/project/list/list-item-form/reusable-list-item-form.component.ts index 6128906c27..28402ae964 100644 --- a/apps/dsp-app/src/app/project/list/list-item-form/reusable-list-item-form.component.ts +++ b/apps/dsp-app/src/app/project/list/list-item-form/reusable-list-item-form.component.ts @@ -1,6 +1,5 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { StringLiteral } from '@dasch-swiss/dsp-js'; import { MultiLanguages } from '@dasch-swiss/vre/shared/app-string-literal'; import { Subscription } from 'rxjs'; import { startWith } from 'rxjs/operators'; @@ -41,13 +40,13 @@ export class ReusableListItemFormComponent implements OnInit, OnDestroy { this.subscription.unsubscribe(); } - _buildForm() { + private _buildForm() { this.form = this._fb.group({ labels: this._fb.array( this.formData.labels.map(({ language, value }) => this._fb.group({ language, - value: [value, [Validators.maxLength(2000)]], + value: [value, [Validators.required, Validators.maxLength(2000)]], }) ), atLeastOneStringRequired('value') @@ -56,7 +55,7 @@ export class ReusableListItemFormComponent implements OnInit, OnDestroy { this.formData.comments.map(({ language, value }) => this._fb.group({ language, - value: [value, [Validators.maxLength(2000)]], + value: [value, [Validators.required, Validators.maxLength(2000)]], }) ) ), diff --git a/apps/dsp-app/src/app/project/list/list.component.ts b/apps/dsp-app/src/app/project/list/list.component.ts index 885dad3274..9f72b5965c 100644 --- a/apps/dsp-app/src/app/project/list/list.component.ts +++ b/apps/dsp-app/src/app/project/list/list.component.ts @@ -12,7 +12,7 @@ import { ProjectsSelectors, } from '@dasch-swiss/vre/shared/app-state'; import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; -import { Observable, Subject } from 'rxjs'; +import { combineLatest, Observable, Subject } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { AppGlobal } from '../../app-global'; import { DIALOG_LARGE } from '../../main/services/dialog-sizes.constant'; @@ -54,9 +54,11 @@ export class ListComponent extends ProjectBase implements OnInit, OnDestroy { // disable content on small devices disableContent = false; - get list$(): Observable { - return this.listsInProject$.pipe(map(lists => (this.listIri ? lists.find(i => i.id === this.listIri) : null))); - } + private readonly routeListIri$ = this._route.paramMap.pipe(map(params => params.get(RouteConstants.listParameter))); + + list$ = combineLatest([this._store.select(ListsSelectors.listsInProject), this.routeListIri$]).pipe( + map(([lists, listIri]) => lists.find(i => i.id.includes(listIri))) + ); @Select(ListsSelectors.isListsLoading) isListsLoading$: Observable; @Select(ListsSelectors.listsInProject) listsInProject$: Observable; diff --git a/apps/dsp-app/src/app/project/ontology-classes/ontology-class-instance/ontology-class-instance.component.ts b/apps/dsp-app/src/app/project/ontology-classes/ontology-class-instance/ontology-class-instance.component.ts index ee14a55f31..bbe215c08f 100644 --- a/apps/dsp-app/src/app/project/ontology-classes/ontology-class-instance/ontology-class-instance.component.ts +++ b/apps/dsp-app/src/app/project/ontology-classes/ontology-class-instance/ontology-class-instance.component.ts @@ -110,6 +110,7 @@ export class OntologyClassInstanceComponent extends ProjectBase implements OnIni ? { query: this._setGravsearch(classId), mode: 'gravsearch', + classId, } : null ) diff --git a/apps/dsp-app/src/app/project/ontology/ontology-form/ontology-form.component.ts b/apps/dsp-app/src/app/project/ontology/ontology-form/ontology-form.component.ts index 3397373a0a..d38b1dd025 100644 --- a/apps/dsp-app/src/app/project/ontology/ontology-form/ontology-form.component.ts +++ b/apps/dsp-app/src/app/project/ontology/ontology-form/ontology-form.component.ts @@ -11,7 +11,6 @@ import { UpdateOntologyMetadata, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { OntologyService, ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { ClearProjectOntologiesAction, @@ -23,9 +22,9 @@ import { SetCurrentOntologyAction, SetCurrentProjectOntologyPropertiesAction, } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, Select, Store, ofActionSuccessful } from '@ngxs/store'; +import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; import { Observable, Subject } from 'rxjs'; -import { take, takeUntil } from 'rxjs/operators'; +import { take, takeUntil, tap } from 'rxjs/operators'; import { existingNamesValidator } from '../../../main/directive/existing-name/existing-names.validator'; import { CustomRegex } from '../../../workspace/resource/values/custom-regex'; @@ -115,7 +114,6 @@ export class OntologyFormComponent implements OnInit, OnDestroy { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _fb: UntypedFormBuilder, private _route: ActivatedRoute, private _router: Router, @@ -247,21 +245,20 @@ export class OntologyFormComponent implements OnInit, OnDestroy { this._dspApiConnection.v2.onto .updateOntology(ontologyData) - .pipe(take(1)) - .subscribe( - (response: OntologyMetadata) => { - this.loadOntologies(response.id); - this.updateParent.emit(response.id); - this.closeDialog.emit(response.id); - }, - (error: ApiResponseError) => { - // in case of an error - this.loading = false; - this.error = true; - - this._errorHandler.showMessage(error); - } - ); + .pipe( + take(1), + tap({ + error: () => { + this.loading = false; + this.error = true; + }, + }) + ) + .subscribe((response: OntologyMetadata) => { + this.loadOntologies(response.id); + this.updateParent.emit(response.id); + this.closeDialog.emit(response.id); + }); } else { // create mode const ontologyData = new CreateOntology(); @@ -287,8 +284,6 @@ export class OntologyFormComponent implements OnInit, OnDestroy { // in case of an error... e.g. because the ontolog iri is not unique, rebuild the form including the error message this.formErrors['name'] += `${this.validationMessages['name']['existingName']} `; this.loading = false; - - this._errorHandler.showMessage(error); } ); } diff --git a/apps/dsp-app/src/app/project/ontology/ontology.component.ts b/apps/dsp-app/src/app/project/ontology/ontology.component.ts index 633c03589a..8d4c4a6450 100644 --- a/apps/dsp-app/src/app/project/ontology/ontology.component.ts +++ b/apps/dsp-app/src/app/project/ontology/ontology.component.ts @@ -14,7 +14,6 @@ import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { - ApiResponseError, ClassDefinition, Constants, DeleteResourceClass, @@ -28,7 +27,6 @@ import { } from '@dasch-swiss/dsp-js'; import { getAllEntityDefinitionsAsArray } from '@dasch-swiss/vre/shared/app-api'; import { DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DefaultClass, DefaultProperties, @@ -53,8 +51,8 @@ import { SetCurrentProjectOntologyPropertiesAction, UserSelectors, } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, Select, Store, ofActionSuccessful } from '@ngxs/store'; -import { Observable, Subject, combineLatest } from 'rxjs'; +import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; +import { combineLatest, Observable, Subject } from 'rxjs'; import { map, take, takeUntil } from 'rxjs/operators'; import { DialogComponent, DialogEvent } from '../../main/dialog/dialog.component'; import { ProjectBase } from '../project-base'; @@ -147,7 +145,6 @@ export class OntologyComponent extends ProjectBase implements OnInit, OnDestroy @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _fb: UntypedFormBuilder, private _ontologyService: OntologyService, private _sortingService: SortingService, @@ -551,26 +548,21 @@ export class OntologyComponent extends ProjectBase implements OnInit, OnDestroy this._dspApiConnection.v2.onto .deleteOntology(updateOntology) .pipe(take(1)) - .subscribe( - () => { - this._store.dispatch(new ClearProjectOntologiesAction(this.projectUuid)); - // reset current ontology - // this._store.dispatch([ - // new SetCurrentOntologyAction(null), - // new RemoveProjectOntologyAction(updateOntology.id, this.projectUuid) - // ]); - // get the ontologies for this project - this.initOntologiesList(); - // go to project ontology page - const goto = `/project/${this.projectUuid}`; - this._router.navigateByUrl(goto, { - skipLocationChange: false, - }); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe(() => { + this._store.dispatch(new ClearProjectOntologiesAction(this.projectUuid)); + // reset current ontology + // this._store.dispatch([ + // new SetCurrentOntologyAction(null), + // new RemoveProjectOntologyAction(updateOntology.id, this.projectUuid) + // ]); + // get the ontologies for this project + this.initOntologiesList(); + // go to project ontology page + const goto = `/project/${this.projectUuid}`; + this._router.navigateByUrl(goto, { + skipLocationChange: false, + }); + }); break; case 'ResourceClass': @@ -581,15 +573,10 @@ export class OntologyComponent extends ProjectBase implements OnInit, OnDestroy this._dspApiConnection.v2.onto .deleteResourceClass(resClass) .pipe(take(1)) - .subscribe( - () => { - this.ontoClasses = []; - this.initOntologiesList(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe(() => { + this.ontoClasses = []; + this.initOntologiesList(); + }); break; case 'Property': // delete resource property and refresh the view @@ -599,18 +586,13 @@ export class OntologyComponent extends ProjectBase implements OnInit, OnDestroy this._dspApiConnection.v2.onto .deleteResourceProperty(resProp) .pipe(take(1)) - .subscribe( - () => { - this._store.dispatch(new ClearCurrentOntologyAction()); - // get the ontologies for this project - this.initOntologiesList(); - // update the view of resource class or list of properties - this.initOntology(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe(() => { + this._store.dispatch(new ClearCurrentOntologyAction()); + // get the ontologies for this project + this.initOntologiesList(); + // update the view of resource class or list of properties + this.initOntology(); + }); break; } } diff --git a/apps/dsp-app/src/app/project/ontology/property-form/property-form.component.ts b/apps/dsp-app/src/app/project/ontology/property-form/property-form.component.ts index a4ad266d2d..9a56f3073d 100644 --- a/apps/dsp-app/src/app/project/ontology/property-form/property-form.component.ts +++ b/apps/dsp-app/src/app/project/ontology/property-form/property-form.component.ts @@ -24,7 +24,6 @@ import { } from '@dasch-swiss/dsp-js'; import { getAllEntityDefinitionsAsArray } from '@dasch-swiss/vre/shared/app-api'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DefaultProperties, DefaultProperty, @@ -42,7 +41,7 @@ import { } from '@dasch-swiss/vre/shared/app-state'; import { Select, Store } from '@ngxs/store'; import { Observable, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { takeUntil, tap } from 'rxjs/operators'; import { DialogEvent } from '../../../main/dialog/dialog.component'; import { existingNamesValidator } from '../../../main/directive/existing-name/existing-names.validator'; import { CustomRegex } from '../../../workspace/resource/values/custom-regex'; @@ -180,7 +179,6 @@ export class PropertyFormComponent implements OnInit, OnDestroy { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _fb: UntypedFormBuilder, private _os: OntologyService, private _sortingService: SortingService, @@ -452,17 +450,14 @@ export class PropertyFormComponent implements OnInit, OnDestroy { * canEnableRequiredToggle: evaluate if the required toggle can be set for a newly assigned property of a class */ canEnableRequiredToggle() { - this._dspApiConnection.v2.onto.canReplaceCardinalityOfResourceClass(this.resClassIri).subscribe( - (response: CanDoResponse) => { + this._dspApiConnection.v2.onto + .canReplaceCardinalityOfResourceClass(this.resClassIri) + .subscribe((response: CanDoResponse) => { if (response.canDo) { // enable the form this.propertyForm.controls['required'].enable(); } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + }); } /** @@ -484,19 +479,14 @@ export class PropertyFormComponent implements OnInit, OnDestroy { const targetCardinality: Cardinality = this.getTargetCardinality(targetGuiCardinality); this._dspApiConnection.v2.onto .canReplaceCardinalityOfResourceClassWith(this.resClassIri, this.propertyInfo.propDef.id, targetCardinality) - .subscribe( - (response: CanDoResponse) => { - this.canSetCardinality = response.canDo; - if (!this.canSetCardinality) { - this.canNotSetCardinalityReason = response.cannotDoReason; - this.canNotSetCardinalityUiReason = this.getCanNotSetCardinalityUserReason(); - } - this.canChangeCardinalityChecked = true; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + .subscribe((response: CanDoResponse) => { + this.canSetCardinality = response.canDo; + if (!this.canSetCardinality) { + this.canNotSetCardinalityReason = response.cannotDoReason; + this.canNotSetCardinalityUiReason = this.getCanNotSetCardinalityUserReason(); } - ); + this.canChangeCardinalityChecked = true; + }); } /** @@ -607,7 +597,6 @@ export class PropertyFormComponent implements OnInit, OnDestroy { (error: ApiResponseError) => { this.error = true; this.loading = false; - this._errorHandler.showMessage(error); } ); } @@ -615,17 +604,20 @@ export class PropertyFormComponent implements OnInit, OnDestroy { createNewPropertyAndAssignToClass() { const onto = this.getOntologyForNewProperty(); // create new property and assign it to the class - this._dspApiConnection.v2.onto.createResourceProperty(onto).subscribe( - (response: ResourcePropertyDefinitionWithAllLanguages) => { + this._dspApiConnection.v2.onto + .createResourceProperty(onto) + .pipe( + tap({ + error: () => { + this.error = true; + this.loading = false; + }, + }) + ) + .subscribe((response: ResourcePropertyDefinitionWithAllLanguages) => { this.lastModificationDate = response.lastModificationDate; this.assignProperty(response); - }, - (error: ApiResponseError) => { - this.error = true; - this.loading = false; - this._errorHandler.showMessage(error); - } - ); + }); } /** @@ -911,7 +903,6 @@ export class PropertyFormComponent implements OnInit, OnDestroy { onError(err) { this.error = true; this.loading = false; - this._errorHandler.showMessage(err); } /** diff --git a/apps/dsp-app/src/app/project/ontology/property-info/property-info.component.ts b/apps/dsp-app/src/app/project/ontology/property-info/property-info.component.ts index 24aaeed5d4..6d61f36d98 100644 --- a/apps/dsp-app/src/app/project/ontology/property-info/property-info.component.ts +++ b/apps/dsp-app/src/app/project/ontology/property-info/property-info.component.ts @@ -1,7 +1,6 @@ import { animate, state, style, transition, trigger } from '@angular/animations'; import { AfterContentInit, Component, EventEmitter, Inject, Input, OnChanges, Output } from '@angular/core'; import { - ApiResponseError, CanDoResponse, Constants, KnoraApiConnection, @@ -10,7 +9,6 @@ import { } from '@dasch-swiss/dsp-js'; import { getAllEntityDefinitionsAsArray } from '@dasch-swiss/vre/shared/app-api'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DefaultClass, DefaultProperties, @@ -117,7 +115,6 @@ export class PropertyInfoComponent implements OnChanges, AfterContentInit { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _ontoService: OntologyService, private _store: Store ) {} @@ -204,14 +201,9 @@ export class PropertyInfoComponent implements OnChanges, AfterContentInit { */ canBeDeleted(): void { // check if the property can be deleted - this._dspApiConnection.v2.onto.canDeleteResourceProperty(this.propDef.id).subscribe( - (canDoRes: CanDoResponse) => { - this.propCanBeDeleted = canDoRes.canDo; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.onto.canDeleteResourceProperty(this.propDef.id).subscribe((canDoRes: CanDoResponse) => { + this.propCanBeDeleted = canDoRes.canDo; + }); } /** diff --git a/apps/dsp-app/src/app/project/ontology/resource-class-form/resource-class-form.component.ts b/apps/dsp-app/src/app/project/ontology/resource-class-form/resource-class-form.component.ts index 1c704e84a9..c8681ee400 100644 --- a/apps/dsp-app/src/app/project/ontology/resource-class-form/resource-class-form.component.ts +++ b/apps/dsp-app/src/app/project/ontology/resource-class-form/resource-class-form.component.ts @@ -67,7 +67,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy { // add all resource properties to the same list resourceProperties.forEach((resProp: PropertyDefinition) => { const name = this._os.getNameFromIri(resProp.id); - this.existingNames.push(new RegExp(`(?:^|W)${name.toLowerCase()}(?:$|W)`)); + this.existingNames.push(new RegExp(`(?:^|W)${name}(?:$|W)`)); }); this.buildForm(); @@ -84,7 +84,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy { this.formData.name, [ Validators.required, - existingNamesValidator(this.existingNames), + existingNamesValidator(this.existingNames, true), Validators.pattern(CustomRegex.ID_NAME_REGEX), ], ], diff --git a/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-info.component.ts b/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-info.component.ts index f054886c73..863879eda4 100644 --- a/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-info.component.ts +++ b/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-info.component.ts @@ -11,7 +11,6 @@ import { } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { - ApiResponseError, CanDoResponse, ClassDefinition, Constants, @@ -21,28 +20,27 @@ import { ResourcePropertyDefinitionWithAllLanguages, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DefaultClass, - DefaultResourceClasses, - SortingService, DefaultProperties, DefaultProperty, + DefaultResourceClasses, + OntologyService, PropertyCategory, PropertyInfoObject, - OntologyService, + SortingService, } from '@dasch-swiss/vre/shared/app-helper-services'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { OntologiesSelectors, OntologyProperties, + PropertyAssignment, PropToAdd, PropToDisplay, - PropertyAssignment, RemovePropertyAction, ReplacePropertyAction, } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, Select, Store, ofActionSuccessful } from '@ngxs/store'; +import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; import { Observable, Subject } from 'rxjs'; import { map, take, takeUntil } from 'rxjs/operators'; import { DialogComponent, DialogEvent } from '../../../main/dialog/dialog.component'; @@ -133,7 +131,6 @@ export class ResourceClassInfoComponent implements OnInit, OnDestroy { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _notification: NotificationService, private _sortingService: SortingService, private _store: Store, @@ -282,14 +279,11 @@ export class ResourceClassInfoComponent implements OnInit, OnDestroy { canBeDeleted() { // check if the class can be deleted - this._dspApiConnection.v2.onto.canDeleteResourceClass(this.resourceClass.id).subscribe( - (response: CanDoResponse) => { + this._dspApiConnection.v2.onto + .canDeleteResourceClass(this.resourceClass.id) + .subscribe((response: CanDoResponse) => { this.classCanBeDeleted = response.canDo; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + }); } addNewProperty(propType: DefaultProperty, currentOntologyPropertiesToDisplay: PropToDisplay[]) { diff --git a/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-property-info/resource-class-property-info.component.ts b/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-property-info/resource-class-property-info.component.ts index 94583658c1..84db8f37cb 100644 --- a/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-property-info/resource-class-property-info.component.ts +++ b/apps/dsp-app/src/app/project/ontology/resource-class-info/resource-class-property-info/resource-class-property-info.component.ts @@ -1,6 +1,5 @@ import { AfterContentInit, Component, EventEmitter, Inject, Input, OnChanges, Output } from '@angular/core'; import { - ApiResponseError, CanDoResponse, Constants, IHasProperty, @@ -10,7 +9,6 @@ import { UpdateResourceClassCardinality, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DefaultClass, DefaultProperty, OntologyService } from '@dasch-swiss/vre/shared/app-helper-services'; import { ListsSelectors, OntologiesSelectors } from '@dasch-swiss/vre/shared/app-state'; import { Store } from '@ngxs/store'; @@ -83,7 +81,6 @@ export class ResourceClassPropertyInfoComponent implements OnChanges, AfterConte constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _ontoService: OntologyService, private _store: Store ) {} @@ -180,14 +177,8 @@ export class ResourceClassPropertyInfoComponent implements OnChanges, AfterConte delCard.cardinalities = [this.propCard]; onto.entity = delCard; - this._dspApiConnection.v2.onto.canDeleteCardinalityFromResourceClass(onto).subscribe( - (canDoRes: CanDoResponse) => { - this.propCanBeRemovedFromClass = canDoRes.canDo; - }, - // open snackbar displaying the error - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.onto.canDeleteCardinalityFromResourceClass(onto).subscribe((canDoRes: CanDoResponse) => { + this.propCanBeRemovedFromClass = canDoRes.canDo; + }); } } diff --git a/apps/dsp-app/src/app/project/reusable-project-form/reusable-project-form.component.ts b/apps/dsp-app/src/app/project/reusable-project-form/reusable-project-form.component.ts index 814304b13e..5ac4258f96 100644 --- a/apps/dsp-app/src/app/project/reusable-project-form/reusable-project-form.component.ts +++ b/apps/dsp-app/src/app/project/reusable-project-form/reusable-project-form.component.ts @@ -1,10 +1,13 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ProjectsSelectors } from '@dasch-swiss/vre/shared/app-state'; import { MultiLanguages } from '@dasch-swiss/vre/shared/app-string-literal'; -import { Subscription } from 'rxjs'; +import { Select, Store } from '@ngxs/store'; +import { Observable, Subscription } from 'rxjs'; import { startWith } from 'rxjs/operators'; import { arrayLengthGreaterThanZeroValidator } from './array-length-greater-than-zero-validator'; import { atLeastOneStringRequired } from './at-least-one-string-required.validator'; +import { shortcodeExistsValidator } from './shortcode-exists.validator'; @Component({ selector: 'app-reusable-project-form', @@ -15,7 +18,7 @@ import { atLeastOneStringRequired } from './at-least-one-string-required.validat [formGroup]="form" controlName="shortcode" [placeholder]="'appLabels.form.project.general.shortcode' | translate" - [validatorErrors]="[shortcodePatternError]" + [validatorErrors]="[shortcodePatternError, shortCodeExistsError]" data-cy="shortcode-input" style="flex: 1; margin-right: 16px"> @@ -41,11 +44,11 @@ import { atLeastOneStringRequired } from './at-least-one-string-required.validat + editable="true"> + [validators]="keywordsValidators"> `, }) @@ -61,9 +64,14 @@ export class ReusableProjectFormComponent implements OnInit, OnDestroy { form: FormGroup; shortcodePatternError = { errorKey: 'pattern', message: 'This field must contains letters from A to F and 0 to 9' }; + shortCodeExistsError = { errorKey: 'shortcodeExists', message: 'This shortcode already exists' }; + readonly keywordsValidators = [Validators.minLength(3), Validators.maxLength(64)]; subscription: Subscription; - constructor(private _fb: FormBuilder) {} + constructor( + private _fb: FormBuilder, + private _store: Store + ) {} ngOnInit() { this._buildForm(); @@ -74,10 +82,18 @@ export class ReusableProjectFormComponent implements OnInit, OnDestroy { } private _buildForm() { + const existingShortcodes = this._store.selectSnapshot(ProjectsSelectors.allProjectShortcodes); + this.form = this._fb.group({ shortcode: [ { value: this.formData.shortcode, disabled: this.formData.shortcode !== '' }, - [Validators.required, Validators.minLength(4), Validators.maxLength(4), Validators.pattern(/^[0-9A-Fa-f]+$/)], + [ + Validators.required, + Validators.minLength(4), + Validators.maxLength(4), + Validators.pattern(/^[0-9A-Fa-f]+$/), + shortcodeExistsValidator(existingShortcodes), + ], ], shortname: [ { value: this.formData.shortname, disabled: this.formData.shortname !== '' }, @@ -93,7 +109,12 @@ export class ReusableProjectFormComponent implements OnInit, OnDestroy { ), atLeastOneStringRequired('value') ), - keywords: [this.formData.keywords, arrayLengthGreaterThanZeroValidator()], + keywords: this._fb.array( + this.formData.keywords.map(keyword => { + return [keyword, this.keywordsValidators]; + }), + arrayLengthGreaterThanZeroValidator() + ), }); } diff --git a/apps/dsp-app/src/app/project/reusable-project-form/shortcode-exists.validator.ts b/apps/dsp-app/src/app/project/reusable-project-form/shortcode-exists.validator.ts new file mode 100644 index 0000000000..17ee1c540b --- /dev/null +++ b/apps/dsp-app/src/app/project/reusable-project-form/shortcode-exists.validator.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidatorFn, ValidationErrors } from '@angular/forms'; + +/** + * shortcodeValidator: Validate the shortcode value against + * already existing shortcodes. + * @param shortcodes - existing shortcodes + */ +export function shortcodeExistsValidator(shortcodes: string[]): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return shortcodes.includes(control.value) ? { shortcodeExists: true } : null; + }; +} diff --git a/apps/dsp-app/src/app/system/projects/projects-list/projects-list.component.ts b/apps/dsp-app/src/app/system/projects/projects-list/projects-list.component.ts index 2ad197b2e4..f9e94af78d 100644 --- a/apps/dsp-app/src/app/system/projects/projects-list/projects-list.component.ts +++ b/apps/dsp-app/src/app/system/projects/projects-list/projects-list.component.ts @@ -161,22 +161,15 @@ export class ProjectsListComponent implements OnInit, OnDestroy { } deactivateProject(id: string) { - this._projectApiService.delete(id).pipe( - tap(() => { - this.refreshParent.emit(); // TODO Soft or Hard refresh ? - }) - ); + this._projectApiService.delete(id).subscribe(() => { + this.refreshParent.emit(); + }); } activateProject(id: string) { // As there is no activate route implemented in the js lib, we use the update route to set the status to true - const data: UpdateProjectRequest = new UpdateProjectRequest(); - data.status = true; - - this._projectApiService.update(id, data).pipe( - tap(() => { - this.refreshParent.emit(); - }) - ); + this._projectApiService.update(id, { status: true }).subscribe(() => { + this.refreshParent.emit(); + }); } } diff --git a/apps/dsp-app/src/app/system/users/users-list/users-list.component.ts b/apps/dsp-app/src/app/system/users/users-list/users-list.component.ts index 53c1293306..034a78392a 100644 --- a/apps/dsp-app/src/app/system/users/users-list/users-list.component.ts +++ b/apps/dsp-app/src/app/system/users/users-list/users-list.component.ts @@ -9,10 +9,9 @@ import { } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { ApiResponseError, Constants, Permissions, ReadProject, ReadUser } from '@dasch-swiss/dsp-js'; +import { Constants, Permissions, ReadProject, ReadUser } from '@dasch-swiss/dsp-js'; import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { ProjectService, SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; import { LoadProjectMembersAction, @@ -124,7 +123,6 @@ export class UsersListComponent implements OnInit { private _userApiService: UserApiService, private _matDialog: MatDialog, private _dialog: DialogService, - private _errorHandler: AppErrorHandler, private _route: ActivatedRoute, private _router: Router, private _sortingService: SortingService, @@ -189,51 +187,41 @@ export class UsersListComponent implements OnInit { */ updateGroupsMembership(id: string, groups: string[]): void { const currentUserGroups: string[] = []; - this._userApiService.getGroupMembershipsForUser(id).subscribe( - response => { - for (const group of response.groups) { - currentUserGroups.push(group.id); - } + this._userApiService.getGroupMembershipsForUser(id).subscribe(response => { + for (const group of response.groups) { + currentUserGroups.push(group.id); + } - if (currentUserGroups.length === 0) { - // add user to group - // console.log('add user to group'); - for (const newGroup of groups) { - this.addUserToGroupMembership(id, newGroup); - } - } else { - // which one is deselected? - // find id in groups --> if not exists: remove from group - for (const oldGroup of currentUserGroups) { - if (groups.indexOf(oldGroup) === -1) { - // console.log('remove from group', oldGroup); - // the old group is not anymore one of the selected groups --> remove user from group - this._userApiService - .removeFromGroupMembership(id, oldGroup) - .pipe(take(1)) - .subscribe( - () => { - if (this.projectUuid) { - this._store.dispatch(new LoadProjectMembershipAction(this.projectUuid)); - } - }, - (ngError: ApiResponseError) => { - this._errorHandler.showMessage(ngError); - } - ); - } + if (currentUserGroups.length === 0) { + // add user to group + // console.log('add user to group'); + for (const newGroup of groups) { + this.addUserToGroupMembership(id, newGroup); + } + } else { + // which one is deselected? + // find id in groups --> if not exists: remove from group + for (const oldGroup of currentUserGroups) { + if (groups.indexOf(oldGroup) === -1) { + // console.log('remove from group', oldGroup); + // the old group is not anymore one of the selected groups --> remove user from group + this._userApiService + .removeFromGroupMembership(id, oldGroup) + .pipe(take(1)) + .subscribe(() => { + if (this.projectUuid) { + this._store.dispatch(new LoadProjectMembershipAction(this.projectUuid)); + } + }); } - for (const newGroup of groups) { - if (currentUserGroups.indexOf(newGroup) === -1) { - this.addUserToGroupMembership(id, newGroup); - } + } + for (const newGroup of groups) { + if (currentUserGroups.indexOf(newGroup) === -1) { + this.addUserToGroupMembership(id, newGroup); } } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); } - ); + }); } /** @@ -244,67 +232,57 @@ export class UsersListComponent implements OnInit { const userIsProjectAdmin = this.userIsProjectAdmin(permissions); if (userIsProjectAdmin) { // true = user is already project admin --> remove from admin rights - this._userApiService.removeFromProjectMembership(id, this.project.id, true).subscribe( - response => { - // if this user is not the logged-in user - if (currentUser.username !== response.user.username) { - this._store.dispatch(new SetUserAction(response.user)); - this.refreshParent.emit(); - } else { - // the logged-in user removed himself as project admin - // the list is not available anymore; - // open dialog to confirm and - // redirect to project page - // update the application state of logged-in user and the session - this._store.dispatch(new LoadUserAction(currentUser.username)); - this._actions$ - .pipe(ofActionSuccessful(LoadUserAction)) - .pipe(take(1)) - .subscribe(() => { - const isSysAdmin = ProjectService.IsMemberOfSystemAdminGroup( - (currentUser as ReadUser).permissions.groupsPerProject - ); - if (isSysAdmin) { - this.refreshParent.emit(); - } else { - // logged-in user is NOT system admin: - // go to project page and reload project admin interface - this._router - .navigateByUrl(RouteConstants.refreshRelative, { - skipLocationChange: true, - }) - .then(() => this._router.navigate([`${RouteConstants.projectRelative}/${this.projectUuid}`])); - } - }); - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + this._userApiService.removeFromProjectMembership(id, this.project.id, true).subscribe(response => { + // if this user is not the logged-in user + if (currentUser.username !== response.user.username) { + this._store.dispatch(new SetUserAction(response.user)); + this.refreshParent.emit(); + } else { + // the logged-in user removed himself as project admin + // the list is not available anymore; + // open dialog to confirm and + // redirect to project page + // update the application state of logged-in user and the session + this._store.dispatch(new LoadUserAction(currentUser.username)); + this._actions$ + .pipe(ofActionSuccessful(LoadUserAction)) + .pipe(take(1)) + .subscribe(() => { + const isSysAdmin = ProjectService.IsMemberOfSystemAdminGroup( + (currentUser as ReadUser).permissions.groupsPerProject + ); + if (isSysAdmin) { + this.refreshParent.emit(); + } else { + // logged-in user is NOT system admin: + // go to project page and reload project admin interface + this._router + .navigateByUrl(RouteConstants.refreshRelative, { + skipLocationChange: true, + }) + .then(() => this._router.navigate([`${RouteConstants.projectRelative}/${this.projectUuid}`])); + } + }); } - ); + }); } else { // false: user isn't project admin yet --> add admin rights - this._userApiService.addToProjectMembership(id, this.project.id, true).subscribe( - response => { - if (currentUser.username !== response.user.username) { - this._store.dispatch(new SetUserAction(response.user)); - this.refreshParent.emit(); - } else { - // the logged-in user (system admin) added himself as project admin - // update the application state of logged-in user and the session - this._store.dispatch(new LoadUserAction(currentUser.username)); - this._actions$ - .pipe(ofActionSuccessful(LoadUserAction)) - .pipe(take(1)) - .subscribe(() => { - this.refreshParent.emit(); - }); - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + this._userApiService.addToProjectMembership(id, this.project.id, true).subscribe(response => { + if (currentUser.username !== response.user.username) { + this._store.dispatch(new SetUserAction(response.user)); + this.refreshParent.emit(); + } else { + // the logged-in user (system admin) added himself as project admin + // update the application state of logged-in user and the session + this._store.dispatch(new LoadUserAction(currentUser.username)); + this._actions$ + .pipe(ofActionSuccessful(LoadUserAction)) + .pipe(take(1)) + .subscribe(() => { + this.refreshParent.emit(); + }); } - ); + }); } } @@ -312,17 +290,12 @@ export class UsersListComponent implements OnInit { this._userApiService .updateSystemAdminMembership(user.id, systemAdmin) .pipe(take(1)) - .subscribe( - response => { - this._store.dispatch(new SetUserAction(response.user)); - if (this._store.selectSnapshot(UserSelectors.username) !== user.username) { - this.refreshParent.emit(); - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + .subscribe(response => { + this._store.dispatch(new SetUserAction(response.user)); + if (this._store.selectSnapshot(UserSelectors.username) !== user.username) { + this.refreshParent.emit(); } - ); + }); } askToActivateUser(username: string, id: string) { @@ -401,15 +374,10 @@ export class UsersListComponent implements OnInit { this._userApiService .delete(id) .pipe(take(1)) - .subscribe( - response => { - this._store.dispatch(new SetUserAction(response.user)); - this.refreshParent.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe(response => { + this._store.dispatch(new SetUserAction(response.user)); + this.refreshParent.emit(); + }); } /** @@ -421,15 +389,10 @@ export class UsersListComponent implements OnInit { this._userApiService .updateStatus(id, true) .pipe(take(1)) - .subscribe( - response => { - this._store.dispatch(new SetUserAction(response.user)); - this.refreshParent.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe(response => { + this._store.dispatch(new SetUserAction(response.user)); + this.refreshParent.emit(); + }); } sortList(key: any) { diff --git a/apps/dsp-app/src/app/user/membership/membership.component.html b/apps/dsp-app/src/app/user/membership/membership.component.html index 34879eda02..d00999d608 100644 --- a/apps/dsp-app/src/app/user/membership/membership.component.html +++ b/apps/dsp-app/src/app/user/membership/membership.component.html @@ -1,6 +1,6 @@ - + -
+

This user is member of {{ (user$ | async)?.projects.length | i18nPlural : itemPluralMapping['project'] }}

@@ -34,8 +34,8 @@

{{project.longname}} ({{project.shortname}})

- - Select Project to add user + + Select Project to add user {{ project?.name }} diff --git a/apps/dsp-app/src/app/user/membership/membership.component.ts b/apps/dsp-app/src/app/user/membership/membership.component.ts index 65ba475148..be016ceb19 100644 --- a/apps/dsp-app/src/app/user/membership/membership.component.ts +++ b/apps/dsp-app/src/app/user/membership/membership.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; import { Constants, ReadUser, StoredProject } from '@dasch-swiss/dsp-js'; import { PermissionsData } from '@dasch-swiss/dsp-js/src/models/admin/permissions-data'; @@ -28,24 +28,25 @@ export interface IPermissions { export class MembershipComponent implements OnDestroy { private ngUnsubscribe: Subject = new Subject(); + selectedValue: string; + @Input() user: ReadUser; @Output() closeDialog: EventEmitter = new EventEmitter(); - get user$(): Observable { - return this.allUsers$.pipe( - takeUntil(this.ngUnsubscribe), - map(users => users.find(u => u.id === this.user.id)) - ); - } + user$: Observable = this._store.select(UserSelectors.allUsers).pipe( + takeUntil(this.ngUnsubscribe), + map(users => users.find(u => u.id === this.user.id)) + ); // get all projects and filter by projects where the user is already member of - get projects$(): Observable { - return combineLatest([this.allProjects$, this.user$]).pipe( - takeUntil(this.ngUnsubscribe), - map(([projects, user]) => this.getProjects(projects, user)) - ); - } + projects$: Observable = combineLatest([ + this._store.select(ProjectsSelectors.allProjects), + this.user$, + ]).pipe( + takeUntil(this.ngUnsubscribe), + map(([projects, user]) => this.getProjects(projects, user)) + ); newProject = new UntypedFormControl(); @@ -58,10 +59,7 @@ export class MembershipComponent implements OnDestroy { }, }; - @Select(ProjectsSelectors.allProjects) allProjects$: Observable; - @Select(UserSelectors.allUsers) allUsers$: Observable; - @Select(ProjectsSelectors.isProjectsLoading) - isProjectsLoading$: Observable; + @Select(ProjectsSelectors.isMembershipLoading) isMembershipLoading$: Observable; constructor(private _store: Store) {} @@ -77,6 +75,7 @@ export class MembershipComponent implements OnDestroy { */ removeFromProject(iri: string) { this._store.dispatch(new RemoveUserFromProjectAction(this.user.id, iri)); + this.selectedValue = ''; } addToProject(iri: string) { @@ -100,32 +99,29 @@ export class MembershipComponent implements OnDestroy { } private getProjects(projects: StoredProject[], user: ReadUser): AutocompleteItem[] { - // TODO code smell, next line should not be disabled!!! - return ( - projects - // eslint-disable-next-line array-callback-return - .map(p => { - if ( - p.id !== Constants.SystemProjectIRI && - p.id !== Constants.DefaultSharedOntologyIRI && - p.status === true && - user.projects.findIndex(i => i.id === p.id) === -1 - ) { - return { - iri: p.id, - name: `${p.longname} (${p.shortname})`, - }; - } - }) - .sort((u1: AutocompleteItem, u2: AutocompleteItem) => { - if (u1.name < u2.name) { - return -1; - } else if (u1.name > u2.name) { - return 1; - } else { - return 0; + return projects + .filter( + p => + p.id !== Constants.SystemProjectIRI && + p.id !== Constants.DefaultSharedOntologyIRI && + p.status === true && + user.projects.findIndex(i => i.id === p.id) === -1 + ) + .map( + p => + { + iri: p.id, + name: `${p.longname} (${p.shortname})`, } - }) - ); + ) + .sort((u1: AutocompleteItem, u2: AutocompleteItem) => { + if (u1.name < u2.name) { + return -1; + } else if (u1.name > u2.name) { + return 1; + } else { + return 0; + } + }); } } diff --git a/apps/dsp-app/src/app/user/user-form/password-form/password-form.component.ts b/apps/dsp-app/src/app/user/user-form/password-form/password-form.component.ts index 6fdc22b714..91cc1d3bf1 100644 --- a/apps/dsp-app/src/app/user/user-form/password-form/password-form.component.ts +++ b/apps/dsp-app/src/app/user/user-form/password-form/password-form.component.ts @@ -3,7 +3,6 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } import { ApiResponseError, KnoraApiConnection, ReadUser, User } from '@dasch-swiss/dsp-js'; import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { UserSelectors } from '@dasch-swiss/vre/shared/app-state'; import { Store } from '@ngxs/store'; @@ -74,7 +73,6 @@ export class PasswordFormComponent implements OnInit { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _userApiService: UserApiService, - private _errorHandler: AppErrorHandler, private _fb: UntypedFormBuilder, private _notification: NotificationService, private store: Store @@ -249,8 +247,6 @@ export class PasswordFormComponent implements OnInit { this.form.controls.requesterPassword.setErrors({ incorrectPassword: true, }); - } else { - this._errorHandler.showMessage(error); } this.loading = false; this.error = true; diff --git a/apps/dsp-app/src/app/user/user-form/user-form.component.ts b/apps/dsp-app/src/app/user/user-form/user-form.component.ts index 5654d8fc1a..c87dd11681 100644 --- a/apps/dsp-app/src/app/user/user-form/user-form.component.ts +++ b/apps/dsp-app/src/app/user/user-form/user-form.component.ts @@ -3,7 +3,6 @@ import { ChangeDetectorRef, Component, EventEmitter, - Inject, Input, OnChanges, OnInit, @@ -12,8 +11,6 @@ import { import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { ApiResponseError, Constants, ReadUser, StringLiteral, UpdateUserRequest, User } from '@dasch-swiss/dsp-js'; import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { @@ -25,7 +22,7 @@ import { } from '@dasch-swiss/vre/shared/app-state'; import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; import { combineLatest, Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { take, tap } from 'rxjs/operators'; import { AppGlobal } from '../../app-global'; import { existingNamesValidator } from '../../main/directive/existing-name/existing-names.validator'; import { CustomRegex } from '../../workspace/resource/values/custom-regex'; @@ -148,7 +145,6 @@ export class UserFormComponent implements OnInit, OnChanges { constructor( private _userApiService: UserApiService, - private _errorHandler: AppErrorHandler, private _formBuilder: UntypedFormBuilder, private _notification: NotificationService, private _projectService: ProjectService, @@ -314,8 +310,16 @@ export class UserFormComponent implements OnInit, OnChanges { // userData.email = this.userForm.value.email; userData.lang = this.userForm.value.lang; - this._userApiService.updateBasicInformation(this.user.id, userData).subscribe( - response => { + this._userApiService + .updateBasicInformation(this.user.id, userData) + .pipe( + tap({ + error: () => { + this.loading = false; + }, + }) + ) + .subscribe(response => { this.user = response.user; this.buildForm(this.user); const user = this._store.selectSnapshot(UserSelectors.user) as ReadUser; @@ -329,12 +333,7 @@ export class UserFormComponent implements OnInit, OnChanges { this._notification.openSnackBar("You have successfully updated the user's profile data."); this.closeDialog.emit(); this.loading = false; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - this.loading = false; - } - ); + }); } else { this.createNewUser(this.userForm.value); } diff --git a/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts b/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts index 3f4fa7a3be..32f112cd69 100644 --- a/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts +++ b/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts @@ -5,7 +5,7 @@ import { User } from '@dasch-swiss/dsp-js'; import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { AuthService } from '@dasch-swiss/vre/shared/app-session'; import { UserSelectors } from '@dasch-swiss/vre/shared/app-state'; -import { Select } from '@ngxs/store'; +import { Select, Store } from '@ngxs/store'; import { Observable, Subject } from 'rxjs'; import { MenuItem } from '../../main/declarations/menu-item'; @@ -23,9 +23,7 @@ export class UserMenuComponent implements OnInit, OnDestroy { private ngUnsubscribe: Subject = new Subject(); - get isLoggedIn$(): Observable { - return this._authService.isSessionValid$(); - } + isLoggedIn$ = this._store.select(UserSelectors.isLoggedIn); @Select(UserSelectors.user) user$: Observable; @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; @@ -34,7 +32,7 @@ export class UserMenuComponent implements OnInit, OnDestroy { constructor( private _authService: AuthService, - private _router: Router + private _store: Store ) {} ngOnInit() { diff --git a/apps/dsp-app/src/app/workspace/intermediate/intermediate.component.ts b/apps/dsp-app/src/app/workspace/intermediate/intermediate.component.ts index da4096efd5..f3b90523e2 100644 --- a/apps/dsp-app/src/app/workspace/intermediate/intermediate.component.ts +++ b/apps/dsp-app/src/app/workspace/intermediate/intermediate.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DialogComponent } from '../../main/dialog/dialog.component'; import { FilteredResources } from '../results/list-view/list-view.component'; @@ -24,10 +23,7 @@ export class IntermediateComponent { }, }; - constructor( - private _dialog: MatDialog, - private _errorHandler: AppErrorHandler - ) {} + constructor(private _dialog: MatDialog) {} /** * opens the dialog box with a form to create a link resource, to edit resources etc. diff --git a/apps/dsp-app/src/app/workspace/resource/operations/create-link-resource/create-link-resource.component.ts b/apps/dsp-app/src/app/workspace/resource/operations/create-link-resource/create-link-resource.component.ts index 3ee1bc5066..cc8d064f7a 100644 --- a/apps/dsp-app/src/app/workspace/resource/operations/create-link-resource/create-link-resource.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/operations/create-link-resource/create-link-resource.component.ts @@ -1,7 +1,6 @@ import { Component, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { - ApiResponseError, Constants, CreateFileValue, CreateResource, @@ -15,7 +14,6 @@ import { } from '@dasch-swiss/dsp-js'; import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DialogEvent } from '../../../../main/dialog/dialog.component'; import { SelectPropertiesComponent } from '../../resource-instance-form/select-properties/select-properties.component'; @@ -49,8 +47,7 @@ export class CreateLinkResourceComponent implements OnInit { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _projectApiService: ProjectApiService, - private _fb: UntypedFormBuilder, - private _errorHandler: AppErrorHandler + private _fb: UntypedFormBuilder ) {} ngOnInit(): void { @@ -144,14 +141,9 @@ export class CreateLinkResourceComponent implements OnInit { createResource.properties = this.propertiesObj; - this._dspApiConnection.v2.res.createResource(createResource).subscribe( - (res: ReadResource) => { - this.closeDialog.emit(res); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.res.createResource(createResource).subscribe((res: ReadResource) => { + this.closeDialog.emit(res); + }); }); } else { this.propertiesForm.markAllAsTouched(); diff --git a/apps/dsp-app/src/app/workspace/resource/properties/properties.component.ts b/apps/dsp-app/src/app/workspace/resource/properties/properties.component.ts index a7f5b3ea74..1a8e2a5d57 100644 --- a/apps/dsp-app/src/app/workspace/resource/properties/properties.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/properties/properties.component.ts @@ -38,17 +38,16 @@ import { } from '@dasch-swiss/dsp-js'; import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { OntologyService, ProjectService, SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { LoadClassItemsCountAction } from '@dasch-swiss/vre/shared/app-state'; import { Store } from '@ngxs/store'; -import { Observable, Subscription, forkJoin } from 'rxjs'; +import { forkJoin, Observable, Subscription } from 'rxjs'; import { ConfirmationWithComment, DialogComponent } from '../../../main/dialog/dialog.component'; import { - Events as CommsEvents, ComponentCommunicationEventService, EmitEvent, + Events as CommsEvents, } from '../../../main/services/component-communication-event.service'; import { DspResource } from '../dsp-resource'; import { RepresentationConstants } from '../representation/file-representation'; @@ -166,7 +165,6 @@ export class PropertiesComponent implements OnInit, OnChanges, OnDestroy { private _dspApiConnection: KnoraApiConnection, private _projectApiService: ProjectApiService, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _incomingService: IncomingService, private _notification: NotificationService, private _resourceService: ResourceService, @@ -342,26 +340,16 @@ export class PropertiesComponent implements OnInit, OnChanges, OnDestroy { switch (type) { case 'delete': // delete the resource and refresh the view - this._dspApiConnection.v2.res.deleteResource(payload).subscribe( - (response: DeleteResourceResponse) => { - this._onResourceDeleted(response); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.res.deleteResource(payload).subscribe((response: DeleteResourceResponse) => { + this._onResourceDeleted(response); + }); break; case 'erase': // erase the resource and refresh the view - this._dspApiConnection.v2.res.eraseResource(payload).subscribe( - (response: DeleteResourceResponse) => { - this._onResourceDeleted(response); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.res.eraseResource(payload).subscribe((response: DeleteResourceResponse) => { + this._onResourceDeleted(response); + }); break; } } else if (this.resource.res.label !== answer.comment) { @@ -374,8 +362,9 @@ export class PropertiesComponent implements OnInit, OnChanges, OnDestroy { payload.lastModificationDate = res.lastModificationDate; payload.label = answer.comment; - this._dspApiConnection.v2.res.updateResourceMetadata(payload).subscribe( - (response: UpdateResourceMetadataResponse) => { + this._dspApiConnection.v2.res + .updateResourceMetadata(payload) + .subscribe((response: UpdateResourceMetadataResponse) => { this.resource.res.label = payload.label; this.lastModificationDate = response.lastModificationDate; // if annotations tab is active; a label of a region has been changed --> update regions @@ -384,11 +373,7 @@ export class PropertiesComponent implements OnInit, OnChanges, OnDestroy { this.regionChanged.emit(); } this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + }); }); } } diff --git a/apps/dsp-app/src/app/workspace/resource/representation/archive/archive.component.ts b/apps/dsp-app/src/app/workspace/resource/representation/archive/archive.component.ts index 73cbe7bd78..45eaee8998 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/archive/archive.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/archive/archive.component.ts @@ -1,7 +1,6 @@ import { AfterViewInit, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { - ApiResponseError, Constants, KnoraApiConnection, ReadArchiveFileValue, @@ -12,7 +11,6 @@ import { WriteValueResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { mergeMap } from 'rxjs/operators'; import { DialogComponent } from '../../../../main/dialog/dialog.component'; import { @@ -44,7 +42,6 @@ export class ArchiveComponent implements OnInit, AfterViewInit { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _rs: RepresentationService, private _valueOperationEventService: ValueOperationEventService ) {} @@ -109,32 +106,25 @@ export class ArchiveComponent implements OnInit, AfterViewInit { this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid) ) ) - .subscribe( - (res2: ReadResource) => { - this.src.fileValue.fileUrl = ( - res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue - ).fileUrl; - this.src.fileValue.filename = ( - res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue - ).filename; - this.src.fileValue.strval = ( - res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue - ).strval; - - this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { - this.originalFilename = res['originalFilename']; - - this._valueOperationEventService.emit( - new EmitEvent( - Events.FileValueUpdated, - new UpdatedFileEventValue(res2.properties[Constants.HasArchiveFileValue][0]) - ) - ); - }); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe((res2: ReadResource) => { + this.src.fileValue.fileUrl = ( + res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue + ).fileUrl; + this.src.fileValue.filename = ( + res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue + ).filename; + this.src.fileValue.strval = (res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue).strval; + + this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { + this.originalFilename = res['originalFilename']; + + this._valueOperationEventService.emit( + new EmitEvent( + Events.FileValueUpdated, + new UpdatedFileEventValue(res2.properties[Constants.HasArchiveFileValue][0]) + ) + ); + }); + }); } } diff --git a/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.html b/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.html index c59b36418a..d8e5af49b4 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.html +++ b/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.html @@ -11,7 +11,7 @@
Your browser does not support the audio element.
-
diff --git a/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.ts b/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.ts index fdb02ab2a1..3048d22458 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/audio/audio.component.ts @@ -2,7 +2,6 @@ import { AfterViewInit, Component, EventEmitter, Inject, Input, OnInit, Output } import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { - ApiResponseError, Constants, KnoraApiConnection, ReadAudioFileValue, @@ -13,7 +12,6 @@ import { WriteValueResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { mergeMap } from 'rxjs/operators'; import { DialogComponent } from '../../../../main/dialog/dialog.component'; import { @@ -47,7 +45,6 @@ export class AudioComponent implements OnInit, AfterViewInit { private _dspApiConnection: KnoraApiConnection, private _sanitizer: DomSanitizer, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _rs: RepresentationService, private _valueOperationEventService: ValueOperationEventService ) {} @@ -66,12 +63,10 @@ export class AudioComponent implements OnInit, AfterViewInit { ngAfterViewInit() { this.loaded.emit(true); - const player = document.getElementById('audio') as HTMLAudioElement; - if (player) { - player.addEventListener('timeupdate', () => { - this.currentTime = player.currentTime; - }); - } + } + + onTimeUpdate(event: { target: HTMLAudioElement }) { + this.currentTime = event.target.currentTime; } togglePlay() { @@ -174,36 +169,29 @@ export class AudioComponent implements OnInit, AfterViewInit { this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid) ) ) - .subscribe( - (res2: ReadResource) => { - this.src.fileValue.fileUrl = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).fileUrl; - this.src.fileValue.filename = ( - res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue - ).filename; - this.src.fileValue.strval = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).strval; - this.src.fileValue.valueCreationDate = ( - res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue - ).valueCreationDate; - - this.audio = this._sanitizer.bypassSecurityTrustUrl(this.src.fileValue.fileUrl); - - this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { - this.originalFilename = res['originalFilename']; - }); - - this._valueOperationEventService.emit( - new EmitEvent( - Events.FileValueUpdated, - new UpdatedFileEventValue(res2.properties[Constants.HasAudioFileValue][0]) - ) - ); - - const audioElem = document.getElementById('audio'); - (audioElem as HTMLAudioElement).load(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe((res2: ReadResource) => { + this.src.fileValue.fileUrl = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).fileUrl; + this.src.fileValue.filename = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).filename; + this.src.fileValue.strval = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).strval; + this.src.fileValue.valueCreationDate = ( + res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue + ).valueCreationDate; + + this.audio = this._sanitizer.bypassSecurityTrustUrl(this.src.fileValue.fileUrl); + + this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { + this.originalFilename = res['originalFilename']; + }); + + this._valueOperationEventService.emit( + new EmitEvent( + Events.FileValueUpdated, + new UpdatedFileEventValue(res2.properties[Constants.HasAudioFileValue][0]) + ) + ); + + const audioElem = document.getElementById('audio'); + (audioElem as HTMLAudioElement).load(); + }); } } diff --git a/apps/dsp-app/src/app/workspace/resource/representation/document/document.component.ts b/apps/dsp-app/src/app/workspace/resource/representation/document/document.component.ts index f49de8da3f..ff39a1b97b 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/document/document.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/document/document.component.ts @@ -2,7 +2,6 @@ import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Component, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { - ApiResponseError, Constants, KnoraApiConnection, ReadDocumentFileValue, @@ -13,7 +12,6 @@ import { WriteValueResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { PdfViewerComponent } from 'ng2-pdf-viewer'; import { mergeMap } from 'rxjs/operators'; import { DialogComponent } from '../../../../main/dialog/dialog.component'; @@ -57,7 +55,6 @@ export class DocumentComponent implements OnInit, AfterViewInit { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _rs: RepresentationService, private _valueOperationEventService: ValueOperationEventService ) {} @@ -183,43 +180,38 @@ export class DocumentComponent implements OnInit, AfterViewInit { this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid) ) ) - .subscribe( - (res2: ReadResource) => { - this.src.fileValue.fileUrl = ( - res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue - ).fileUrl; - this.src.fileValue.filename = ( - res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue - ).filename; - this.src.fileValue.strval = ( - res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue - ).strval; - this.src.fileValue.valueCreationDate = ( - res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue - ).valueCreationDate; - - this.fileType = this._getFileType(this.src.fileValue.filename); - if (this.fileType === 'pdf') { - this.elem = document.getElementsByClassName('pdf-viewer')[0]; - } - - this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { - this.originalFilename = res['originalFilename']; - }); - - this.zoomFactor = 1.0; - this.pdfQuery = ''; - - this._valueOperationEventService.emit( - new EmitEvent( - Events.FileValueUpdated, - new UpdatedFileEventValue(res2.properties[Constants.HasDocumentFileValue][0]) - ) - ); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + .subscribe((res2: ReadResource) => { + this.src.fileValue.fileUrl = ( + res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue + ).fileUrl; + this.src.fileValue.filename = ( + res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue + ).filename; + this.src.fileValue.strval = ( + res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue + ).strval; + this.src.fileValue.valueCreationDate = ( + res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue + ).valueCreationDate; + + this.fileType = this._getFileType(this.src.fileValue.filename); + if (this.fileType === 'pdf') { + this.elem = document.getElementsByClassName('pdf-viewer')[0]; } - ); + + this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { + this.originalFilename = res['originalFilename']; + }); + + this.zoomFactor = 1.0; + this.pdfQuery = ''; + + this._valueOperationEventService.emit( + new EmitEvent( + Events.FileValueUpdated, + new UpdatedFileEventValue(res2.properties[Constants.HasDocumentFileValue][0]) + ) + ); + }); } } diff --git a/apps/dsp-app/src/app/workspace/resource/representation/representation.service.ts b/apps/dsp-app/src/app/workspace/resource/representation/representation.service.ts index da2671bd1a..3df75dda6b 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/representation.service.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/representation.service.ts @@ -1,17 +1,12 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; -import { Observable, throwError } from 'rxjs'; -import { catchError } from 'rxjs/operators'; +import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class RepresentationService { - constructor( - private _errorHandler: AppErrorHandler, - private readonly _http: HttpClient - ) {} + constructor(private readonly _http: HttpClient) {} /** * returns info about a file @@ -46,14 +41,7 @@ export class RepresentationService { pathToJson = `${url.substring(0, url.lastIndexOf('/'))}/knora.json`; } - return this._http.get(pathToJson, requestOptions).pipe( - catchError(error => { - // handle error in app - this._errorHandler.showMessage(error); - // throw console & logging service - return throwError(error); - }) - ); + return this._http.get(pathToJson, requestOptions); } /** @@ -64,30 +52,26 @@ export class RepresentationService { async downloadFile(url: string, imageFilename?: string) { let originalFilename; - try { - const res = await this._http.get(url, { responseType: 'blob', withCredentials: true }).toPromise(); + const res = await this._http.get(url, { responseType: 'blob', withCredentials: true }).toPromise(); - await this.getFileInfo(url, imageFilename).subscribe(response => { - originalFilename = response['originalFilename']; + await this.getFileInfo(url, imageFilename).subscribe(response => { + originalFilename = response['originalFilename']; - const objUrl = window.URL.createObjectURL(res); - const e = document.createElement('a'); - e.href = objUrl; + const objUrl = window.URL.createObjectURL(res); + const e = document.createElement('a'); + e.href = objUrl; - // set filename - if (originalFilename === undefined) { - e.download = url.substring(url.lastIndexOf('/') + 1); - } else { - e.download = originalFilename; - } + // set filename + if (originalFilename === undefined) { + e.download = url.substring(url.lastIndexOf('/') + 1); + } else { + e.download = originalFilename; + } - document.body.appendChild(e); - e.click(); - document.body.removeChild(e); - }); - } catch (e) { - this._errorHandler.showMessage(e); - } + document.body.appendChild(e); + e.click(); + document.body.removeChild(e); + }); } /** diff --git a/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.html b/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.html index 96725665cc..391821686e 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.html +++ b/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.html @@ -76,7 +76,9 @@ Copy IIIF URL to clipboard - + @@ -141,7 +143,7 @@ mat-icon-button id="DSP_OSD_DRAW_REGION" matTooltip="Draw Region" - [disabled]="failedToLoad" + [disabled]="failedToLoad || !adminPermissions" (click)="drawButtonClicked()" [class.active]="regionDrawMode"> diff --git a/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.ts b/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.ts index fed6a4bfab..cea6464055 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/still-image/still-image.component.ts @@ -1,4 +1,3 @@ -import { HttpClient } from '@angular/common/http'; import { AfterViewInit, Component, @@ -16,7 +15,6 @@ import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { - ApiResponseError, Constants, CreateColorValue, CreateGeomValue, @@ -37,7 +35,6 @@ import { WriteValueResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import * as OpenSeadragon from 'openseadragon'; import { Subscription } from 'rxjs'; @@ -123,6 +120,7 @@ export class StillImageComponent implements OnChanges, OnDestroy, AfterViewInit @Input() compoundNavigation?: DspCompoundPosition; @Input() currentTab: string; @Input() parentResource: ReadResource; + @Input() adminPermissions: boolean; @Output() goToPage = new EventEmitter(); @@ -147,11 +145,9 @@ export class StillImageComponent implements OnChanges, OnDestroy, AfterViewInit constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private readonly _http: HttpClient, private _dialog: MatDialog, private _domSanitizer: DomSanitizer, private _elementRef: ElementRef, - private _errorHandler: AppErrorHandler, private _matIconRegistry: MatIconRegistry, private _notification: NotificationService, private _renderer: Renderer2, @@ -169,6 +165,7 @@ export class StillImageComponent implements OnChanges, OnDestroy, AfterViewInit this._domSanitizer.bypassSecurityTrustResourceUrl('/assets/images/draw-region-icon.svg') ); } + /** * calculates the surface of a rectangular region. * @@ -437,25 +434,20 @@ export class StillImageComponent implements OnChanges, OnDestroy, AfterViewInit this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid) ) ) - .subscribe( - (res2: ReadResource) => { - this._valueOperationEventService.emit( - new EmitEvent( - Events.FileValueUpdated, - new UpdatedFileEventValue(res2.properties[Constants.HasStillImageFileValue][0]) - ) - ); - - this._rs - .getFileInfo(this.images[0].fileValue.fileUrl, this.images[0].fileValue.filename) - .subscribe((res: { originalFilename: string }) => { - this.originalFilename = res.originalFilename; - }); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe((res2: ReadResource) => { + this._valueOperationEventService.emit( + new EmitEvent( + Events.FileValueUpdated, + new UpdatedFileEventValue(res2.properties[Constants.HasStillImageFileValue][0]) + ) + ); + + this._rs + .getFileInfo(this.images[0].fileValue.fileUrl, this.images[0].fileValue.filename) + .subscribe((res: { originalFilename: string }) => { + this.originalFilename = res.originalFilename; + }); + }); } /** @@ -491,6 +483,7 @@ export class StillImageComponent implements OnChanges, OnDestroy, AfterViewInit } }); } + /** * uploads the region after being prepared by the dialog * @param startPoint the start point of the drawing @@ -536,14 +529,9 @@ export class StillImageComponent implements OnChanges, OnDestroy, AfterViewInit [Constants.IsRegionOfValue]: [linkVal], [Constants.HasGeometry]: [geomVal], }; - this._dspApiConnection.v2.res.createResource(createResource).subscribe( - (res: ReadResource) => { - this.regionAdded.emit(res.id); - }, - error => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.res.createResource(createResource).subscribe((res: ReadResource) => { + this.regionAdded.emit(res.id); + }); } /** diff --git a/apps/dsp-app/src/app/workspace/resource/representation/text/text.component.ts b/apps/dsp-app/src/app/workspace/resource/representation/text/text.component.ts index 7453f1a0bf..a5c022f36b 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/text/text.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/text/text.component.ts @@ -1,18 +1,16 @@ import { AfterViewInit, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { - ApiResponseError, Constants, KnoraApiConnection, - ReadTextFileValue, ReadResource, + ReadTextFileValue, UpdateFileValue, UpdateResource, UpdateValue, WriteValueResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { mergeMap } from 'rxjs/operators'; import { DialogComponent } from '../../../../main/dialog/dialog.component'; import { @@ -44,7 +42,6 @@ export class TextComponent implements OnInit, AfterViewInit { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, private _rs: RepresentationService, private _valueOperationEventService: ValueOperationEventService ) {} @@ -109,26 +106,21 @@ export class TextComponent implements OnInit, AfterViewInit { this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid) ) ) - .subscribe( - (res2: ReadResource) => { - this.src.fileValue.fileUrl = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).fileUrl; - this.src.fileValue.filename = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).filename; - this.src.fileValue.strval = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).strval; - - this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { - this.originalFilename = res['originalFilename']; - }); - - this._valueOperationEventService.emit( - new EmitEvent( - Events.FileValueUpdated, - new UpdatedFileEventValue(res2.properties[Constants.HasTextFileValue][0]) - ) - ); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe((res2: ReadResource) => { + this.src.fileValue.fileUrl = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).fileUrl; + this.src.fileValue.filename = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).filename; + this.src.fileValue.strval = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).strval; + + this._rs.getFileInfo(this.src.fileValue.fileUrl).subscribe(res => { + this.originalFilename = res['originalFilename']; + }); + + this._valueOperationEventService.emit( + new EmitEvent( + Events.FileValueUpdated, + new UpdatedFileEventValue(res2.properties[Constants.HasTextFileValue][0]) + ) + ); + }); } } diff --git a/apps/dsp-app/src/app/workspace/resource/representation/upload/upload-file.service.ts b/apps/dsp-app/src/app/workspace/resource/representation/upload/upload-file.service.ts index 6028de18bf..c2a4c3b45b 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/upload/upload-file.service.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/upload/upload-file.service.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { AppConfigService } from '@dasch-swiss/vre/shared/app-config'; -import { AuthService } from '@dasch-swiss/vre/shared/app-session'; +import { AccessTokenService, AuthService } from '@dasch-swiss/vre/shared/app-session'; import { Observable } from 'rxjs'; export interface UploadedFile { @@ -22,7 +22,7 @@ export class UploadFileService { constructor( private readonly _acs: AppConfigService, private readonly _http: HttpClient, - private readonly _authService: AuthService + private readonly _accessTokenService: AccessTokenService ) {} /** @@ -32,7 +32,7 @@ export class UploadFileService { upload(file: FormData): Observable { const uploadUrl = `${this._acs.dspIiifConfig.iiifUrl}/upload`; - const jwt = this._authService.getAccessToken(); + const jwt = this._accessTokenService.getAccessToken(); const params = new HttpParams().set('token', jwt); // --> TODO in order to track the progress change below to true and 'events' diff --git a/apps/dsp-app/src/app/workspace/resource/representation/upload/upload.component.ts b/apps/dsp-app/src/app/workspace/resource/representation/upload/upload.component.ts index ff768fb7e9..b1919d4a55 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/upload/upload.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/upload/upload.component.ts @@ -17,7 +17,6 @@ import { UpdateStillImageFileValue, UpdateTextFileValue, } from '@dasch-swiss/dsp-js'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { UploadedFileResponse, UploadFileService } from './upload-file.service'; @@ -60,8 +59,7 @@ export class UploadComponent implements OnInit { private _fb: UntypedFormBuilder, private _notification: NotificationService, private _sanitizer: DomSanitizer, - private _upload: UploadFileService, - private _errorHandler: AppErrorHandler + private _upload: UploadFileService ) {} ngOnInit(): void { @@ -132,7 +130,6 @@ export class UploadComponent implements OnInit { this.isLoading = false; this.file = null; this.thumbnailUrl = null; - this._errorHandler.handleError(e); this.forceReload.emit(); } ); diff --git a/apps/dsp-app/src/app/workspace/resource/representation/video/video.component.ts b/apps/dsp-app/src/app/workspace/resource/representation/video/video.component.ts index a90bad1064..bbdd087875 100644 --- a/apps/dsp-app/src/app/workspace/resource/representation/video/video.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/representation/video/video.component.ts @@ -14,7 +14,6 @@ import { import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { - ApiResponseError, Constants, KnoraApiConnection, ReadMovingImageFileValue, @@ -25,7 +24,6 @@ import { WriteValueResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { mergeMap } from 'rxjs/operators'; import { DialogComponent } from '../../../../main/dialog/dialog.component'; @@ -122,7 +120,6 @@ export class VideoComponent implements OnChanges, AfterViewInit { private _dialog: MatDialog, private _sanitizer: DomSanitizer, private _rs: RepresentationService, - private _errorHandler: AppErrorHandler, private _notification: NotificationService, private _valueOperationEventService: ValueOperationEventService ) {} @@ -249,6 +246,7 @@ export class VideoComponent implements OnChanges, AfterViewInit { updateTimeFromSlider(time: number) { this._navigate(time); } + /** * video navigation from scroll event * @@ -314,12 +312,8 @@ export class VideoComponent implements OnChanges, AfterViewInit { } async downloadVideo(url: string) { - try { - const res = await this._http.get(url, { responseType: 'blob', withCredentials: true }).toPromise(); - this.downloadFile(res); - } catch (e) { - this._errorHandler.showMessage(e); - } + const res = await this._http.get(url, { responseType: 'blob', withCredentials: true }).toPromise(); + this.downloadFile(res); } downloadFile(data) { @@ -413,33 +407,28 @@ export class VideoComponent implements OnChanges, AfterViewInit { this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid) ) ) - .subscribe( - (res2: ReadResource) => { - this.src.fileValue.fileUrl = ( - res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue - ).fileUrl; - this.src.fileValue.filename = ( - res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue - ).filename; - this.src.fileValue.strval = ( - res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue - ).strval; - - this.ngOnChanges(); - - this.loadedMetadata(); - - this._valueOperationEventService.emit( - new EmitEvent( - Events.FileValueUpdated, - new UpdatedFileEventValue(res2.properties[Constants.HasMovingImageFileValue][0]) - ) - ); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + .subscribe((res2: ReadResource) => { + this.src.fileValue.fileUrl = ( + res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue + ).fileUrl; + this.src.fileValue.filename = ( + res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue + ).filename; + this.src.fileValue.strval = ( + res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue + ).strval; + + this.ngOnChanges(); + + this.loadedMetadata(); + + this._valueOperationEventService.emit( + new EmitEvent( + Events.FileValueUpdated, + new UpdatedFileEventValue(res2.properties[Constants.HasMovingImageFileValue][0]) + ) + ); + }); } /** diff --git a/apps/dsp-app/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts b/apps/dsp-app/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts index 218a038847..ed9d68fc56 100644 --- a/apps/dsp-app/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts @@ -24,15 +24,15 @@ import { ResourcePropertyDefinition, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { DefaultClass, DefaultResourceClasses } from '@dasch-swiss/vre/shared/app-helper-services'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { LoadClassItemsCountAction } from '@dasch-swiss/vre/shared/app-state'; import { Store } from '@ngxs/store'; +import { tap } from 'rxjs/operators'; import { - Events as CommsEvents, ComponentCommunicationEventService, EmitEvent, + Events as CommsEvents, } from '../../../main/services/component-communication-event.service'; import { ResourceService } from '../services/resource.service'; import { SelectPropertiesComponent } from './select-properties/select-properties.component'; @@ -87,7 +87,6 @@ export class ResourceInstanceFormComponent implements OnInit, OnChanges { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _fb: UntypedFormBuilder, private _resourceService: ResourceService, private _route: ActivatedRoute, @@ -127,8 +126,17 @@ export class ResourceInstanceFormComponent implements OnInit, OnChanges { this.preparing = true; this.loading = true; this._dspApiConnection.v2.ontologyCache.reloadCachedItem(this.ontologyIri).subscribe(() => { - this._dspApiConnection.v2.ontologyCache.getResourceClassDefinition(resourceClassIri).subscribe( - (onto: ResourceClassAndPropertyDefinitions) => { + this._dspApiConnection.v2.ontologyCache + .getResourceClassDefinition(resourceClassIri) + .pipe( + tap({ + error: () => { + this.preparing = false; + this.loading = false; + }, + }) + ) + .subscribe((onto: ResourceClassAndPropertyDefinitions) => { this.ontologyInfo = onto; this.resourceClass = onto.classes[resourceClassIri]; @@ -174,13 +182,7 @@ export class ResourceInstanceFormComponent implements OnInit, OnChanges { this.preparing = false; this.loading = false; this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this.preparing = false; - this.loading = false; - this._errorHandler.showMessage(error); - } - ); + }); }); } @@ -241,8 +243,17 @@ export class ResourceInstanceFormComponent implements OnInit, OnChanges { } createResource.properties = this.propertiesObj; - this._dspApiConnection.v2.res.createResource(createResource).subscribe( - (res: ReadResource) => { + this._dspApiConnection.v2.res + .createResource(createResource) + .pipe( + tap({ + error: () => { + this.error = true; + this.loading = false; + }, + }) + ) + .subscribe((res: ReadResource) => { this.resource = res; const uuid = this._resourceService.getResourceUuid(this.resource.id); @@ -257,20 +268,7 @@ export class ResourceInstanceFormComponent implements OnInit, OnChanges { this._componentCommsService.emit(new EmitEvent(CommsEvents.resourceCreated)); this._cd.markForCheck(); }); - }, - (error: ApiResponseError) => { - this.error = true; - this.loading = false; - if (error.status === 400) { - this._notification.openSnackBar( - 'Bad request(400): There was an issue with your request. Often this is due to duplicate values in one of your properties.', - 'error' - ); - } else { - this._errorHandler.showMessage(error); - } - } - ); + }); } else { this.propertiesParentForm.markAllAsTouched(); } diff --git a/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.html b/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.html index d681d233be..bf1080d679 100644 --- a/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.html +++ b/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.html @@ -2,7 +2,7 @@ [formGroup]="form" (ngSubmit)="submitData()" class="form" - *ngIf="(usersProjects$ | async)?.length; else notProjectMember"> + *ngIf="(usersProjects$ | async)?.length && (isCurrentProjectAdminOrSysAdmin$ | async) === true; else notProjectMember">

The following resources will be connected:

    -
  • {{ res.label }}
  • +
  • {{ res.label }}
diff --git a/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.ts b/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.ts index 2285553d6e..a8eed73c07 100644 --- a/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/resource-link-form/resource-link-form.component.ts @@ -11,7 +11,6 @@ import { import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { - ApiResponseError, Constants, CreateLinkValue, CreateResource, @@ -21,12 +20,11 @@ import { StoredProject, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { ProjectsSelectors, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; -import { Select } from '@ngxs/store'; -import { Observable, Subject, combineLatest } from 'rxjs'; +import { Select, Store } from '@ngxs/store'; +import { combineLatest, Observable, Subject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; -import { FilteredResources } from '../../results/list-view/list-view.component'; +import { FilteredResources, ShortResInfo } from '../../results/list-view/list-view.component'; import { ResourceService } from '../services/resource.service'; @Component({ @@ -59,30 +57,29 @@ export class ResourceLinkFormComponent implements OnInit, OnDestroy { selectedProject: string; - get usersProjects$(): Observable { - return combineLatest([this.currentUserProjects$, this.isSysAdmin$, this.allNotSystemProjects$]).pipe( - takeUntil(this.ngUnsubscribe), - map(([currentUserProjects, isSysAdmin, allNotSystemProjects]) => - isSysAdmin ? currentUserProjects : allNotSystemProjects - ) - ); - } - - @Select(ProjectsSelectors.allNotSystemProjects) - allNotSystemProjects$: Observable; - @Select(UserSelectors.userProjects) currentUserProjects$: Observable; - @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; + usersProjects$: Observable = combineLatest([ + this._store.select(UserSelectors.userProjects), + this._store.select(UserSelectors.isSysAdmin), + this._store.select(ProjectsSelectors.allNotSystemProjects), + ]).pipe( + takeUntil(this.ngUnsubscribe), + map(([currentUserProjects, isSysAdmin, allNotSystemProjects]) => + isSysAdmin ? currentUserProjects : allNotSystemProjects + ) + ); + + @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; + @Select(ProjectsSelectors.isCurrentProjectAdminOrSysAdmin) isCurrentProjectAdminOrSysAdmin$: Observable; @Select(ProjectsSelectors.isProjectsLoading) isLoading$: Observable; - @Select(ProjectsSelectors.hasLoadingErrors) - hasLoadingErrors$: Observable; + @Select(ProjectsSelectors.hasLoadingErrors) hasLoadingErrors$: Observable; constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _fb: UntypedFormBuilder, private _resourceService: ResourceService, - private _router: Router + private _router: Router, + private _store: Store ) {} ngOnInit(): void { @@ -128,6 +125,8 @@ export class ResourceLinkFormComponent implements OnInit, OnDestroy { }); } + trackByFn = (index: number, item: ShortResInfo) => `${index}-${item.id}`; + /** * submits the data */ @@ -165,16 +164,11 @@ export class ResourceLinkFormComponent implements OnInit, OnDestroy { }; } - this._dspApiConnection.v2.res.createResource(linkObj).subscribe( - (res: ReadResource) => { - const path = this._resourceService.getResourcePath(res.id); - const goto = `/resource${path}`; - this._router.navigate([]).then(() => window.open(goto, '_blank')); - this.closeDialog.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.res.createResource(linkObj).subscribe((res: ReadResource) => { + const path = this._resourceService.getResourcePath(res.id); + const goto = `/resource${path}`; + this._router.navigate([]).then(() => window.open(goto, '_blank')); + this.closeDialog.emit(); + }); } } diff --git a/apps/dsp-app/src/app/workspace/resource/resource.component.html b/apps/dsp-app/src/app/workspace/resource/resource.component.html index 21bf26d047..efe6239514 100644 --- a/apps/dsp-app/src/app/workspace/resource/resource.component.html +++ b/apps/dsp-app/src/app/workspace/resource/resource.component.html @@ -23,6 +23,7 @@ [currentTab]="selectedTabLabel" [parentResource]="incomingResource ? incomingResource.res : resource.res" [activateRegion]="selectedRegion" + [adminPermissions]="isAdmin$ | async" (loaded)="representationLoaded($event)" (goToPage)="compoundNavigation($event)" (regionClicked)="openRegion($event)" diff --git a/apps/dsp-app/src/app/workspace/resource/resource.component.ts b/apps/dsp-app/src/app/workspace/resource/resource.component.ts index 9f271de4e9..a7541ea3ba 100644 --- a/apps/dsp-app/src/app/workspace/resource/resource.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/resource.component.ts @@ -11,7 +11,7 @@ import { } from '@angular/core'; import { MatTabChangeEvent } from '@angular/material/tabs'; import { Title } from '@angular/platform-browser'; -import { ActivatedRoute, NavigationError, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { ApiResponseError, Constants, @@ -31,13 +31,12 @@ import { SystemPropertyDefinition, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { UserSelectors } from '@dasch-swiss/vre/shared/app-state'; import { Select } from '@ngxs/store'; -import { Observable, Subject, Subscription, combineLatest } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { combineLatest, Observable, Subject, Subscription } from 'rxjs'; +import { map, takeUntil, tap } from 'rxjs/operators'; import { SplitSize } from '../results/results.component'; import { DspCompoundPosition, DspResource } from './dsp-resource'; import { PropertyInfoValues } from './properties/properties.component'; @@ -136,7 +135,6 @@ export class ResourceComponent implements OnChanges, OnDestroy { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _incomingService: IncomingService, private _notification: NotificationService, private _resourceService: ResourceService, @@ -159,11 +157,6 @@ export class ResourceComponent implements OnChanges, OnDestroy { this._router.events.subscribe(event => { this._titleService.setTitle('Resource view'); - - if (event instanceof NavigationError) { - // present error to user - this._errorHandler.showMessage(event.error); - } }); this.valueOperationEventSubscriptions.push( @@ -296,23 +289,24 @@ export class ResourceComponent implements OnChanges, OnDestroy { } this.stillImageRepresentationsForCompoundResourceSub = this._incomingService .getStillImageRepresentationsForCompoundResource(resource.res.id, 0, true) - .subscribe( - (countQuery: CountQueryResponse) => { - if (countQuery.numberOfResults > 0) { - // this is a compound object - this.compoundPosition = new DspCompoundPosition(countQuery.numberOfResults); - this.compoundNavigation(1); - } else { + .pipe( + tap({ + error: () => { this.loading = false; - } - this._cdr.markForCheck(); - }, - (error: ApiResponseError) => { + this._cdr.markForCheck(); + }, + }) + ) + .subscribe((countQuery: CountQueryResponse) => { + if (countQuery.numberOfResults > 0) { + // this is a compound object + this.compoundPosition = new DspCompoundPosition(countQuery.numberOfResults); + this.compoundNavigation(1); + } else { this.loading = false; - this._errorHandler.showMessage(error); - this._cdr.markForCheck(); } - ); + this._cdr.markForCheck(); + }); } else { this.requestIncomingResources(resource); } @@ -371,30 +365,21 @@ export class ResourceComponent implements OnChanges, OnDestroy { if (this.incomingResourceSub) { this.incomingResourceSub.unsubscribe(); } - this.incomingResourceSub = this._dspApiConnection.v2.res.getResource(iri).subscribe( - (response: ReadResource) => { - const res = new DspResource(response); - - this.incomingResource = res; - this.incomingResource.resProps = this.initProps(response); - this.incomingResource.systemProps = - this.incomingResource.res.entityInfo.getPropertyDefinitionsByType(SystemPropertyDefinition); - - this.representationsToDisplay = this.collectRepresentationsAndAnnotations(this.incomingResource); - if ( - this.representationsToDisplay.length && - this.representationsToDisplay[0].fileValue && - this.compoundPosition - ) { - this.getIncomingRegions(this.incomingResource, 0); - } + this.incomingResourceSub = this._dspApiConnection.v2.res.getResource(iri).subscribe((response: ReadResource) => { + const res = new DspResource(response); - this._cdr.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + this.incomingResource = res; + this.incomingResource.resProps = this.initProps(response); + this.incomingResource.systemProps = + this.incomingResource.res.entityInfo.getPropertyDefinitionsByType(SystemPropertyDefinition); + + this.representationsToDisplay = this.collectRepresentationsAndAnnotations(this.incomingResource); + if (this.representationsToDisplay.length && this.representationsToDisplay[0].fileValue && this.compoundPosition) { + this.getIncomingRegions(this.incomingResource, 0); } - ); + + this._cdr.markForCheck(); + }); } tabChanged(e: MatTabChangeEvent) { @@ -575,25 +560,20 @@ export class ResourceComponent implements OnChanges, OnDestroy { } this.stillImageRepresentationsForCompoundResourceSub = this._incomingService .getStillImageRepresentationsForCompoundResource(this.resource.res.id, offset) - .subscribe( - (incomingImageRepresentations: ReadResourceSequence) => { - if (!this.resource) { - return; // if there is no resource anymore when the response arrives, do nothing - } - if (incomingImageRepresentations.resources.length > 0) { - // set the incoming representations for the current offset only - this.resource.incomingRepresentations = incomingImageRepresentations.resources; - this.getIncomingResource(this.resource.incomingRepresentations[this.compoundPosition.position].id); - } else { - this.loading = false; - this.representationsToDisplay = []; - } - this._cdr.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + .subscribe((incomingImageRepresentations: ReadResourceSequence) => { + if (!this.resource) { + return; // if there is no resource anymore when the response arrives, do nothing } - ); + if (incomingImageRepresentations.resources.length > 0) { + // set the incoming representations for the current offset only + this.resource.incomingRepresentations = incomingImageRepresentations.resources; + this.getIncomingResource(this.resource.incomingRepresentations[this.compoundPosition.position].id); + } else { + this.loading = false; + this.representationsToDisplay = []; + } + this._cdr.markForCheck(); + }); } /** @@ -634,8 +614,9 @@ export class ResourceComponent implements OnChanges, OnDestroy { if (this.incomingRegionsSub) { this.incomingRegionsSub.unsubscribe(); } - this.incomingRegionsSub = this._incomingService.getIncomingRegions(resource.res.id, offset).subscribe( - (regions: ReadResourceSequence) => { + this.incomingRegionsSub = this._incomingService + .getIncomingRegions(resource.res.id, offset) + .subscribe((regions: ReadResourceSequence) => { // append elements of regions.resources to resource.incoming Array.prototype.push.apply(resource.incomingAnnotations, regions.resources); @@ -645,11 +626,7 @@ export class ResourceComponent implements OnChanges, OnDestroy { // triggers ngOnChanges of StillImageComponent this.representationsToDisplay = this.collectRepresentationsAndAnnotations(resource); this._cdr.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + }); } /** @@ -659,19 +636,16 @@ export class ResourceComponent implements OnChanges, OnDestroy { * It takes the number of images returned as an argument. */ protected getIncomingLinks(offset: number): void { - this._incomingService.getIncomingLinksForResource(this.resource?.res.id, offset).subscribe( - (incomingResources: ReadResourceSequence) => { + this._incomingService + .getIncomingLinksForResource(this.resource?.res.id, offset) + .subscribe((incomingResources: ReadResourceSequence) => { // Check if incomingReferences is initialized, if not, initialize it as an empty array if (!this.resource?.res.incomingReferences) { this.resource.res.incomingReferences = []; } // append elements incomingResources to this.resource.incomingLinks Array.prototype.push.apply(this.resource?.res.incomingReferences, incomingResources.resources); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + }); } openRegion(iri: string) { diff --git a/apps/dsp-app/src/app/workspace/resource/values/list-value/list-value.component.ts b/apps/dsp-app/src/app/workspace/resource/values/list-value/list-value.component.ts index 74857ea846..bf7f5a975d 100644 --- a/apps/dsp-app/src/app/workspace/resource/values/list-value/list-value.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/values/list-value/list-value.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectorRef, Component, Inject, Input, OnChanges, OnDestroy, OnIn import { FormBuilder } from '@angular/forms'; import { MatMenuTrigger } from '@angular/material/menu'; import { - ApiResponseError, CreateListValue, KnoraApiConnection, ListNodeV2, @@ -11,7 +10,6 @@ import { UpdateListValue, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { BaseValueDirective } from '../../../../main/directive/base-value.directive'; @Component({ @@ -36,7 +34,6 @@ export class ListValueComponent extends BaseValueDirective implements OnInit, On @Inject(FormBuilder) protected _fb: FormBuilder, @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _cd: ChangeDetectorRef ) { super(); @@ -65,14 +62,9 @@ export class ListValueComponent extends BaseValueDirective implements OnInit, On const rootNodeIris = this.propertyDef.guiAttributes; for (const rootNodeIri of rootNodeIris) { const trimmedRootNodeIRI = rootNodeIri.substring(7, rootNodeIri.length - 1); - this._dspApiConnection.v2.list.getList(trimmedRootNodeIRI).subscribe( - (response: ListNodeV2) => { - this.listRootNode = response; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this._dspApiConnection.v2.list.getList(trimmedRootNodeIRI).subscribe((response: ListNodeV2) => { + this.listRootNode = response; + }); } } else { this.valueFormControl.setValue(this.displayValue.listNodeLabel); @@ -138,20 +130,15 @@ export class ListValueComponent extends BaseValueDirective implements OnInit, On const rootNodeIris = this.propertyDef.guiAttributes; for (const rootNodeIri of rootNodeIris) { const trimmedRootNodeIRI = rootNodeIri.substring(7, rootNodeIri.length - 1); - this._dspApiConnection.v2.list.getList(trimmedRootNodeIRI).subscribe( - (response: ListNodeV2) => { - if (!response.children.length) { - // this shouldn't happen since users cannot select the root node - this.selectedNodeHierarchy.push(response.label); - } else { - this.selectedNodeHierarchy = this._getHierarchy(nodeIri, response.children); - } - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + this._dspApiConnection.v2.list.getList(trimmedRootNodeIRI).subscribe((response: ListNodeV2) => { + if (!response.children.length) { + // this shouldn't happen since users cannot select the root node + this.selectedNodeHierarchy.push(response.label); + } else { + this.selectedNodeHierarchy = this._getHierarchy(nodeIri, response.children); } - ); + this._cd.markForCheck(); + }); } } diff --git a/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.html b/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.html index e7b730be63..1d118ca308 100644 --- a/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.html +++ b/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.html @@ -40,7 +40,7 @@
-
+

Your search - {{search.query}} - did not match any documents.

@@ -66,12 +66,12 @@
-
+
warning
No results were found for your query.
-
+
warning
It seems like you don’t have the necessary permissions.
diff --git a/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.ts b/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.ts index 7432ce6c9d..5f37eaab32 100644 --- a/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.ts +++ b/apps/dsp-app/src/app/workspace/results/list-view/list-view.component.ts @@ -19,10 +19,11 @@ import { ReadResourceSequence, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; -import { Subject, Subscription, of } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { OntologyClassSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { Store } from '@ngxs/store'; +import { combineLatest, of, Subject, Subscription } from 'rxjs'; +import { map, take, takeUntil, tap } from 'rxjs/operators'; import { ComponentCommunicationEventService, EmitEvent, @@ -45,6 +46,7 @@ export interface SearchParams { mode: 'fulltext' | 'gravsearch'; filter?: IFulltextSearchParams; projectUuid?: string; + classId?: string; } export interface ShortResInfo { @@ -107,15 +109,12 @@ export class ListViewComponent implements OnChanges, OnInit, OnDestroy { resetCheckBoxes = false; - // number of all results + // number of all results including the ones not included as resources in the response bc. the user does not have the permissions to see them numberOfAllResults: number; // progress status loading = true; - // flag to set permission to see resources - hasPermission = false; - currentIndex = 0; currentRangeStart = 1; @@ -132,11 +131,11 @@ export class ListViewComponent implements OnChanges, OnInit, OnDestroy { @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _componentCommsService: ComponentCommunicationEventService, - private _errorHandler: AppErrorHandler, private _notification: NotificationService, private _route: ActivatedRoute, private _router: Router, - private _cd: ChangeDetectorRef + private _cd: ChangeDetectorRef, + private _store: Store ) {} ngOnInit(): void { @@ -268,7 +267,6 @@ export class ListViewComponent implements OnChanges, OnInit, OnDestroy { } this.loading = false; this._cd.markForCheck(); - this._errorHandler.showMessage(countError); } ); } @@ -291,81 +289,78 @@ export class ListViewComponent implements OnChanges, OnInit, OnDestroy { (error: ApiResponseError) => { this.loading = false; this.resources = undefined; - this._errorHandler.showMessage(error); } ); } else if (this.search.mode === 'gravsearch') { - // emit 'gravSearchExecuted' event to the fulltext-search component in order to clear the input field - this._componentCommsService.emit(new EmitEvent(Events.gravSearchExecuted, true)); - - // request the count query if the page index is zero otherwise it is already stored in the numberOfAllResults - const numberOfAllResults$ = - index !== 0 - ? of(this.numberOfAllResults) - : this._dspApiConnection.v2.search - .doExtendedSearchCountQuery(this.search.query) - .pipe(takeUntil(this.ngUnsubscribe)) - .pipe( - map((count: CountQueryResponse) => { - this.numberOfAllResults = count.numberOfResults; - this.currentRangeEnd = this.numberOfAllResults > 25 ? 25 : this.numberOfAllResults; - if (this.numberOfAllResults === 0) { - this._notification.openSnackBar('No resources to display.'); - this.emitSelectedResources(); - this.resources = undefined; - this.loading = false; - this._cd.markForCheck(); - } - - return count.numberOfResults; - }) - ); - - numberOfAllResults$.pipe(takeUntil(this.ngUnsubscribe)).subscribe( - (numberOfAllResults: number) => { - if (this.search.query !== undefined) { - // build the gravsearch query - let gravsearch = this.search.query; - gravsearch = gravsearch.substring(0, gravsearch.search('OFFSET')); - gravsearch = `${gravsearch}OFFSET ${index}`; - - this._dspApiConnection.v2.search - .doExtendedSearch(gravsearch) - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe( - (response: ReadResourceSequence) => { - // if the response does not contain any resources even the search count is greater than 0, - // it means that the user does not have the permissions to see anything: emit an empty result - if (response.resources.length === 0 && this.numberOfAllResults > 0) { - this._notification.openSnackBar('No permission to display the resources.'); - this.emitSelectedResources(); - } - - this.resources = response; - this.hasPermission = !(numberOfAllResults > 0 && this.resources.resources.length === 0); - this.loading = false; - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this.loading = false; - this.resources = undefined; - this._errorHandler.showMessage(error); - } - ); - } else { - this._notification.openSnackBar('The gravsearch query is not set correctly'); - this.resources = undefined; + this.performGravSearch(index); + } + } + + performGravSearch(index: number) { + // emit 'gravSearchExecuted' event to the fulltext-search component in order to clear the input field + this._componentCommsService.emit(new EmitEvent(Events.gravSearchExecuted, true)); + + if (this.search.query === undefined) { + this._notification.openSnackBar('The gravsearch query is not set correctly'); + this.resources = undefined; + this.loading = false; + this._cd.markForCheck(); + return; + } + + // request the count query if the page index is zero otherwise it is already stored in the numberOfAllResults + const numberOfAllResults$ = + index !== 0 + ? of(this.numberOfAllResults) + : this._store.select(OntologyClassSelectors.classItems).pipe( + take(1), + map(classItems => { + this.numberOfAllResults = classItems[this.search.classId] + ? classItems[this.search.classId].classItemsCount + : 0; + this.currentRangeEnd = this.numberOfAllResults > 25 ? 25 : this.numberOfAllResults; + if (this.numberOfAllResults === 0) { + this._notification.openSnackBar('No resources to display.'); + this.emitSelectedResources(); + this.resources = undefined; + this.loading = false; + this._cd.markForCheck(); + } + + return this.numberOfAllResults; + }) + ); + + let gravsearch = this.search.query; + gravsearch = gravsearch.substring(0, gravsearch.search('OFFSET')); + gravsearch = `${gravsearch}OFFSET ${index}`; + + const graveSearchQuery$ = this._dspApiConnection.v2.search + .doExtendedSearch(gravsearch) + .pipe(takeUntil(this.ngUnsubscribe)); + + combineLatest([graveSearchQuery$, numberOfAllResults$]) + .pipe( + tap({ + error: () => { this.loading = false; - this._cd.markForCheck(); - } - }, - (countError: ApiResponseError) => { - // if error is a timeout, keep the loading animation - this.loading = countError.status === 504; - this._errorHandler.showMessage(countError); - this._cd.markForCheck(); + this.resources = undefined; + }, + }) + ) + .subscribe(([response, numberOfAllResults]) => { + response = response as ReadResourceSequence; + // if the response does not contain any resources even the search count is greater than 0, + // it means that the user does not have the permissions to see anything: emit an empty result + if (response.resources.length === 0 && this.numberOfAllResults > 0) { + this._notification.openSnackBar('No permission to display the resources.'); + this.emitSelectedResources(); } - ); - } + + this.resources = response; + + this.loading = false; + this._cd.markForCheck(); + }); } } diff --git a/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.ts b/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.ts index 8b23634658..eab30b79ed 100644 --- a/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.ts +++ b/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.ts @@ -4,7 +4,6 @@ import { Component, ElementRef, EventEmitter, - Inject, Input, OnChanges, OnDestroy, @@ -15,13 +14,12 @@ import { ViewContainerRef, } from '@angular/core'; import { MatMenuTrigger } from '@angular/material/menu'; -import { ApiResponseError, Constants, KnoraApiConnection, ReadProject } from '@dasch-swiss/dsp-js'; +import { ApiResponseError, Constants, ReadProject } from '@dasch-swiss/dsp-js'; import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { Subscription } from 'rxjs'; +import { tap } from 'rxjs/operators'; import { ComponentCommunicationEventService, Events, @@ -118,11 +116,8 @@ export class FulltextSearchComponent implements OnInit, OnChanges, OnDestroy { displayPhonePanel = false; constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, private _projectsApiService: ProjectApiService, private _componentCommsService: ComponentCommunicationEventService, - private _errorHandler: AppErrorHandler, private _overlay: Overlay, private _sortingService: SortingService, private _viewContainerRef: ViewContainerRef, @@ -189,8 +184,16 @@ export class FulltextSearchComponent implements OnInit, OnChanges, OnDestroy { * get all public projects from DSP-API */ getAllProjects(): void { - this._projectsApiService.list().subscribe( - response => { + this._projectsApiService + .list() + .pipe( + tap({ + error: (error: ApiResponseError) => { + this.error = error; + }, + }) + ) + .subscribe(response => { // filter out deactivated projects and system projects this.projects = response.projects.filter(p => p.status === true && !this.hiddenProjects.includes(p.id)); @@ -198,12 +201,7 @@ export class FulltextSearchComponent implements OnInit, OnChanges, OnDestroy { this.project = JSON.parse(localStorage.getItem('currentProject')); } this.projects = this._sortingService.keySortByAlphabetical(this.projects, 'shortname'); - }, - (error: ApiResponseError) => { - this.error = error; - this._errorHandler.showMessage(error); - } - ); + }); } /** diff --git a/apps/dsp-app/src/styles/_elements.scss b/apps/dsp-app/src/styles/_elements.scss index a5ac732897..2e40884211 100644 --- a/apps/dsp-app/src/styles/_elements.scss +++ b/apps/dsp-app/src/styles/_elements.scss @@ -683,7 +683,7 @@ $gc-small: $form-width - $gc-large - 4; top: 3px; border: 1px solid #e4e4e4; border-radius: 14px; - padding: 0; + padding: 4px; background-color: #e4e4e4; z-index: 2; @include box-shadow(); @@ -692,7 +692,7 @@ $gc-small: $form-width - $gc-large - 4; button { // cursor: pointer; border: none; - padding: 2px; + padding: 4px; outline: none; background-color: transparent; color: $primary; @@ -730,7 +730,7 @@ $gc-small: $form-width - $gc-large - 4; } button.info { - cursor: help; + cursor: inherit; } button:hover { diff --git a/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts b/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts index d4d74498be..2573d98c45 100644 --- a/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts +++ b/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts @@ -6,7 +6,7 @@ import { DspInstrumentationConfig, DspInstrumentationToken, } from '@dasch-swiss/vre/shared/app-config'; -import { AuthService } from '@dasch-swiss/vre/shared/app-session'; +import { AccessTokenService, AuthService } from '@dasch-swiss/vre/shared/app-session'; import { datadogRum, RumFetchResourceEventDomainContext } from '@datadog/browser-rum'; import { Observable } from 'rxjs'; import { v5 as uuidv5 } from 'uuid'; @@ -18,6 +18,7 @@ export class DatadogRumService { private buildTag$: Observable = inject(BuildTagToken); private config: DspInstrumentationConfig = inject(DspInstrumentationToken); private authService: AuthService = inject(AuthService); + private _accessTokenService: AccessTokenService = inject(AccessTokenService); constructor() { this.buildTag$.subscribe(tag => { @@ -49,12 +50,12 @@ export class DatadogRumService { // depending on the session state, activate or deactivate the user this.authService - .isSessionValid$() + .isCredentialsValid$() .pipe(takeUntilDestroyed()) .subscribe((isSessionValid: boolean) => { if (isSessionValid) { - if (this.authService.tokenUser) { - const id: string = uuidv5(this.authService.tokenUser, uuidv5.URL); + if (this._accessTokenService.getTokenUser()) { + const id: string = uuidv5(this._accessTokenService.getTokenUser(), uuidv5.URL); this.setActiveUser(id); } else { this.removeActiveUser(); diff --git a/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts b/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts index 599c19c0a1..47b1aa4606 100644 --- a/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts +++ b/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DspInstrumentationConfig, DspInstrumentationToken } from '@dasch-swiss/vre/shared/app-config'; -import { AuthService } from '@dasch-swiss/vre/shared/app-session'; +import { AccessTokenService, AuthService } from '@dasch-swiss/vre/shared/app-session'; @Injectable({ providedIn: 'root', @@ -9,18 +9,22 @@ import { AuthService } from '@dasch-swiss/vre/shared/app-session'; export class PendoAnalyticsService { private config: DspInstrumentationConfig = inject(DspInstrumentationToken); private authService: AuthService = inject(AuthService); + private _accessTokenService: AccessTokenService = inject(AccessTokenService); private environment: string = this.config.environment; constructor() { this.authService - .isSessionValid$() + .isCredentialsValid$() .pipe(takeUntilDestroyed()) .subscribe((isSessionValid: boolean) => { - if (isSessionValid) { - this.setActiveUser(this.authService.tokenUser); - } else { + if (!isSessionValid) { this.removeActiveUser(); + return; } + const token = this._accessTokenService.getTokenUser(); + if (!token) return; + + this.setActiveUser(token); }); } diff --git a/libs/vre/shared/app-api/src/lib/services/admin/list-api.service.ts b/libs/vre/shared/app-api/src/lib/services/admin/list-api.service.ts index 60fffea892..3fa135d3ba 100644 --- a/libs/vre/shared/app-api/src/lib/services/admin/list-api.service.ts +++ b/libs/vre/shared/app-api/src/lib/services/admin/list-api.service.ts @@ -97,10 +97,6 @@ export class ListApiService extends BaseApi { } updateChildNode(iri: string, updatedNode: UpdateChildNodeRequest) { - // TODO this uses normal update endpoint. throwing an error here seems like bad api pattern. - if (updatedNode.name === undefined && updatedNode.labels === undefined && updatedNode.comments === undefined) { - throw new Error('At least one property is expected from the following properties: name, labels, comments.'); - } return this._http.put(this._listRoute(iri), updatedNode); } diff --git a/libs/vre/shared/app-error-handler/src/lib/app-error-handler.spec.ts b/libs/vre/shared/app-error-handler/src/lib/app-error-handler.spec.ts deleted file mode 100644 index 8d8ef48f0c..0000000000 --- a/libs/vre/shared/app-error-handler/src/lib/app-error-handler.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ApiResponseData, ApiResponseError, HealthResponse } from '@dasch-swiss/dsp-js'; -import { AppLoggingService } from '@dasch-swiss/vre/shared/app-logging'; -import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; -import { HttpStatusMsg } from '@dasch-swiss/vre/shared/assets/status-msg'; -import { MockProvider, MockService } from 'ng-mocks'; -import { of } from 'rxjs'; -import { AjaxResponse } from 'rxjs/ajax'; -import { AppErrorHandler } from './app-error-handler'; -import { DataAccessService } from './data-access.service'; - -describe('AppErrorHandler', () => { - let httpTestingController: HttpTestingController; - let service: AppErrorHandler; - const notificationMock = MockService(NotificationService); - const dataAccessMock = MockService(DataAccessService); - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [BrowserAnimationsModule, HttpClientTestingModule, MatSnackBarModule, NoopAnimationsModule], - providers: [ - MockProvider(AppLoggingService), - MockProvider(NotificationService, notificationMock), - MockProvider(DataAccessService, dataAccessMock), - { - provide: HttpStatusMsg, - }, - ], - }); - service = TestBed.inject(AppErrorHandler); - httpTestingController = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpTestingController.verify(); - jest.clearAllMocks(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - // https://www.beyondjava.net/jest-mocking-an-angular-service - it('api is healthy: should call notification service', () => { - const getStatusSpy = jest.spyOn(dataAccessMock, 'getHealthStatus'); - const response = new HealthResponse(); - response.status = true; - getStatusSpy.mockImplementation(() => - of( - ApiResponseData.fromAjaxResponse({ - response, - } as AjaxResponse) - ) - ); - const expectedMessage = { error: 'gaga' }; - service.showMessage(expectedMessage); - expect(getStatusSpy).toHaveBeenCalled(); - expect(notificationMock.openSnackBar).toHaveBeenCalledWith(expectedMessage); - }); -}); diff --git a/libs/vre/shared/app-error-handler/src/lib/app-error-handler.ts b/libs/vre/shared/app-error-handler/src/lib/app-error-handler.ts index 31b1edc8eb..7d7251da60 100644 --- a/libs/vre/shared/app-error-handler/src/lib/app-error-handler.ts +++ b/libs/vre/shared/app-error-handler/src/lib/app-error-handler.ts @@ -1,114 +1,48 @@ -/* - * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - import { HttpErrorResponse } from '@angular/common/http'; -import { ErrorHandler, inject, Injectable } from '@angular/core'; -import { ApiResponseData, ApiResponseError, HealthResponse } from '@dasch-swiss/dsp-js'; -import { AppLoggingService } from '@dasch-swiss/vre/shared/app-logging'; +import { ErrorHandler, Injectable } from '@angular/core'; +import { ApiResponseError } from '@dasch-swiss/dsp-js'; +import { AppConfigService } from '@dasch-swiss/vre/shared/app-config'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; -import { HttpStatusMsg } from '@dasch-swiss/vre/shared/assets/status-msg'; -import { Observable } from 'rxjs'; import { AjaxError } from 'rxjs/ajax'; -import { take } from 'rxjs/operators'; -import { DataAccessService } from './data-access.service'; @Injectable({ providedIn: 'root', }) export class AppErrorHandler implements ErrorHandler { - appLoggingService: AppLoggingService = inject(AppLoggingService); - dataAccessService: DataAccessService = inject(DataAccessService); - notificationService: NotificationService = inject(NotificationService); - httpStatusMsg: HttpStatusMsg = inject(HttpStatusMsg); + constructor( + private _notification: NotificationService, + private _appConfig: AppConfigService + ) {} - /** - * Logs out the error using the logging service. - * @param error the error to log. - */ - handleError(error: Error): void { - if (error instanceof HttpErrorResponse) { - // HTTP related error - this.appLoggingService.error('Caught HttpErrorResponse error', {}, error); - } else if (error instanceof TypeError) { - // Runtime exceptions mostly introduced by Developer's code - this.appLoggingService.error('Caught TypeError', {}, error); - } else if (error instanceof ReferenceError) { - // Runtime exceptions mostly introduced by Developer's code - this.appLoggingService.error('Caught ReferenceError', {}, error); - } else { - // catch-all: catch rest of errors - this.appLoggingService.error('Caught other error', {}, error); + handleError(error: any): void { + if (error instanceof ApiResponseError && error.error instanceof AjaxError) { + // JS-LIB + this.handleHttpError(error.error, error.url); + } else if (error instanceof HttpErrorResponse) { + // ApiServices + this.handleHttpError(error, error.url); + } else if (this._appConfig.dspConfig.environment !== 'prod') { + console.error(error); } } - showMessage(error: ApiResponseError) { - // in case of (internal) server error - const apiServerError = error.error && !(error.error instanceof AjaxError && error.error['response']); - - if (((error.status > 499 && error.status < 600) || apiServerError) && error.status !== 504) { - let status = apiServerError ? 503 : error.status; + private handleHttpError(error: HttpErrorResponse | AjaxError, url: string | null): void { + let message: string; - // check if the api is healthy: - (this.dataAccessService.getHealthStatus() as Observable>).pipe(take(1)).subscribe( - (response: ApiResponseData) => { - if (!response.body.status) { - const healthError: ApiResponseError = { - error: response.body.message, - method: response.method, - status: 500, - url: error.url, - }; - status = 500; - error = healthError; - this.appLoggingService.error(`ERROR ${status}: Server side error — dsp-api is not healthy`); - } else { - this.appLoggingService.error(`ERROR ${status}: Server side error — dsp-api not responding`); - } - }, - (healthError: ApiResponseError) => { - this.appLoggingService.error(`ERROR ${status}: Server side error — dsp-api not responding`, healthError); - error = healthError; - } - ); - - error.status = status; - this.notificationService.openSnackBar(error); - } else if (error.status === 401 && typeof error.error !== 'string') { - // logout if error status is a 401 error and comes from a DSP-JS request - this.dataAccessService.logout().subscribe( - () => { - // reload the page - window.location.reload(); - }, - (logoutError: ApiResponseError) => { - this.notificationService.openSnackBar(logoutError); - if (logoutError.error instanceof AjaxError) { - this.appLoggingService.error(`Logout ajax error`, {}, new Error(logoutError.error['message'])); - } else { - this.appLoggingService.error(`Logout other error`, {}, new Error(logoutError.error)); - } - } - ); + if (error.status === 0) { + message = 'It seems that you are not connected to internet.'; + } else if (error.message.includes('knora.json: 0 Unknown Error')) { + message = 'IIIF server error: The image could not be loaded. Please try again later.'; + } else if (error.status === 404) { + message = 'The requested resource was not found.'; + } else if (error.status === 504) { + message = `There was a timeout issue with one or several requests. + The resource(s) or a part of it cannot be displayed correctly. + Failed on ${url}`; } else { - // open snack bar in any other case - this.notificationService.openSnackBar(error); - // log error to Rollbar (done automatically by simply throwing a new Error) - if (error instanceof ApiResponseError) { - if (error.error && error.error instanceof AjaxError && !error.error['message'].startsWith('ajax error')) { - // the Api response error contains a complex error message from dsp-js-lib - this.appLoggingService.error(`Api response error`, {}, new Error(error.error['message'])); - } else { - const defaultStatusMsg = this.httpStatusMsg.default; - const message = `${defaultStatusMsg[error.status].message} (${error.status}): ${ - defaultStatusMsg[error.status].description - }`; - this.appLoggingService.error(`Error`, {}, new Error(message)); - } - } else { - this.appLoggingService.error(`Error`, {}, new Error(error)); - } + message = 'There is an error on our side. Our team is notified!'; } + + this._notification.openSnackBar(message, 'error'); } } diff --git a/libs/vre/shared/app-notification/src/lib/app-notification/app-notification.service.ts b/libs/vre/shared/app-notification/src/lib/app-notification/app-notification.service.ts index 9377fe4b4d..3a0a03e8a5 100644 --- a/libs/vre/shared/app-notification/src/lib/app-notification/app-notification.service.ts +++ b/libs/vre/shared/app-notification/src/lib/app-notification/app-notification.service.ts @@ -9,59 +9,16 @@ import { AjaxError } from 'rxjs/ajax'; providedIn: 'root', }) export class NotificationService { - constructor( - private _snackBar: MatSnackBar, - private _statusMsg: HttpStatusMsg - ) {} - - // todo: maybe we can add more parameters like: - // action: string = 'x', duration: number = 4200 - // and / or type: 'note' | 'warning' | 'error' | 'success'; which can be used for the panelClass - openSnackBar(notification: string | HttpErrorResponse | ApiResponseError, type?: 'success' | 'error'): void { - let message: string; + constructor(private _snackBar: MatSnackBar) {} + openSnackBar(notification: string, type: 'success' | 'error' = 'success'): void { const conf: MatSnackBarConfig = { duration: 5000, horizontalPosition: 'center', verticalPosition: 'top', - panelClass: type || 'error', + panelClass: type, }; - if (notification instanceof ApiResponseError) { - conf.panelClass = type || 'error'; - notification = notification as ApiResponseError; - if ( - notification.error && - notification.error instanceof AjaxError && - !notification.error['message'].startsWith('ajax error') - ) { - // the Api response error contains a complex error message from dsp-js-lib - message = notification.error['message']; - } else { - const defaultStatusMsg = this._statusMsg.default; - message = `${defaultStatusMsg[notification.status].message} (${notification.status}): `; - - if (notification.status === 504) { - message += `There was a timeout issue with one or several requests. - The resource(s) or a part of it cannot be displayed correctly. - Failed on ${notification.url}`; - conf.duration = undefined; - } else { - message += `${defaultStatusMsg[notification.status].description}`; - } - } - } else { - conf.panelClass = type || 'success'; - if (notification instanceof HttpErrorResponse) { - message = notification.message; - // sipi error - if (message.includes('knora.json: 0 Unknown Error')) { - message = 'IIIF server error: The image could not be loaded. Please try again later.'; - } - } else { - message = notification; - } - } - this._snackBar.open(message, 'x', conf); + this._snackBar.open(notification, 'x', conf); } } diff --git a/libs/vre/shared/app-session/src/index.ts b/libs/vre/shared/app-session/src/index.ts index 28b392d45f..13f1914df3 100644 --- a/libs/vre/shared/app-session/src/index.ts +++ b/libs/vre/shared/app-session/src/index.ts @@ -1,5 +1,7 @@ -export * from './lib/app-session'; export { Session } from './lib/session'; export { CurrentUser } from './lib/session'; export { AuthError } from './lib/error'; export { AuthService } from './lib/auth.service'; +export { AccessTokenService } from './lib/access-token.service'; +export { AutoLoginService } from './lib/auto-login.service'; +export { LocalStorageWatcherService } from './lib/local-storage-watcher.service'; diff --git a/libs/vre/shared/app-session/src/lib/access-token.service.ts b/libs/vre/shared/app-session/src/lib/access-token.service.ts new file mode 100644 index 0000000000..2a0adff8bd --- /dev/null +++ b/libs/vre/shared/app-session/src/lib/access-token.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@angular/core'; +import { Auth } from '@dasch-swiss/vre/shared/app-config'; +import jwt_decode, { JwtPayload } from 'jwt-decode'; + +@Injectable({ providedIn: 'root' }) +export class AccessTokenService { + getAccessToken() { + return localStorage.getItem(Auth.AccessToken); + } + + storeToken(token: string) { + localStorage.setItem(Auth.AccessToken, token); + this.startTokenRefresh(); + } + + removeTokens() { + localStorage.removeItem(Auth.AccessToken); + localStorage.removeItem(Auth.Refresh_token); + } + + private isTokenExpired(token: JwtPayload): boolean { + const date = this.getTokenExpirationDate(token); + if (date == null) { + return false; + } + + return date.setSeconds(date.getSeconds() - 30).valueOf() <= new Date().valueOf(); + } + + private getTokenExpirationDate(decoded: JwtPayload): Date | null { + if (decoded.exp === undefined) { + return null; + } + + const date = new Date(0); + date.setUTCSeconds(decoded.exp); + + return date; + } + + private getTokenExp(token: string): number { + const decoded = jwt_decode(token); + + if (decoded.exp === undefined) { + return 0; + } + + return decoded.exp; + } + + private startTokenRefresh() { + const token = this.getAccessToken(); + + if (!token) { + return; + } + + const exp = this.getTokenExp(token); + const date = new Date(0); + date.setUTCSeconds(exp); + } + + getTokenUser(): string | null { + return this.getAccessToken(); + } + + decodedAccessToken(token: string) { + try { + return jwt_decode(token); + } catch (e) { + return null; + } + } + + isValidToken(decoded: JwtPayload): boolean { + if (decoded === null) { + return false; + } + return this.isTokenExpired(decoded) && decoded.sub !== undefined; + } +} diff --git a/libs/vre/shared/app-session/src/lib/app-session.spec.ts b/libs/vre/shared/app-session/src/lib/app-session.spec.ts deleted file mode 100644 index 5015b5b063..0000000000 --- a/libs/vre/shared/app-session/src/lib/app-session.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { ApiResponseData, CredentialsResponse, ReadUser, UserResponse } from '@dasch-swiss/dsp-js'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { of } from 'rxjs'; -import { AjaxResponse } from 'rxjs/ajax'; -import { SessionService } from './app-session'; - -describe('SessionService', () => { - let service: SessionService; - let mockApiConnection: any; - const mockSession = { - id: 12345, // expired session id - user: { - name: 'test', - jwt: 'jwt', - lang: 'en', - sysAdmin: false, - projectAdmin: [], - }, - }; - - beforeEach(() => { - mockApiConnection = { - admin: { - usersEndpoint: { - getUser: jest.fn(), - }, - }, - v2: { - jsonWebToken: '', - auth: { - checkCredentials: jest.fn(), - }, - }, - }; - - TestBed.configureTestingModule({ - providers: [ - SessionService, - { - provide: DspApiConnectionToken, - useValue: mockApiConnection, - }, - ], - }); - - service = TestBed.inject(SessionService); - }); - - afterEach(() => { - localStorage.removeItem('session'); - jest.clearAllMocks(); - }); - - describe('getSession method', () => { - it('should get session from localstorage', () => { - localStorage.setItem('session', JSON.stringify(mockSession)); - const session = service.getSession(); - expect(session).toEqual(mockSession); - }); - - it('should return null if no session in localstorage', () => { - const session = service.getSession(); - expect(session).toBeNull(); - }); - }); - - describe('setSession method', () => { - it('should set session in localstorage', done => { - const jwt = 'jwt'; - const identifier = 'test'; - const type = 'username'; - - const user = new ReadUser(); - user.id = '12345'; - user.username = 'test'; - - const userRes = new UserResponse(); - userRes.user = user; - - const responseData = ApiResponseData.fromAjaxResponse(new AjaxResponse({} as any, {} as any, {})); - responseData.body = userRes; - - mockApiConnection.admin.usersEndpoint.getUser.mockReturnValue(of(responseData)); - - service.setSession(jwt, identifier, type).subscribe(() => { - const session = JSON.parse(localStorage.getItem('session') || ''); - expect(session.user.name).toEqual(identifier); - expect(session.user.jwt).toEqual(jwt); - done(); - }); - }); - }); - - describe('destroySession method', () => { - it('should destroy the session', () => { - localStorage.setItem('session', JSON.stringify(mockSession)); - let session = service.getSession(); - expect(session).toEqual(mockSession); - - service.destroySession(); - session = service.getSession(); - expect(session).toBeNull(); - }); - }); - - describe('isSessionValid method', () => { - it('should return false if there is no session', () => { - service.isSessionValid().subscribe(isValid => { - expect(isValid).toBeFalsy(); - }); - }); - - it('should return true if session is still valid', () => { - // mock Date.now() - jest.spyOn(Date, 'now').mockImplementation(() => Date.parse('2023-06-16')); - - // create a copy of mockSession - const session = { ...mockSession }; - - // change the id to a more realistic number. - // it's a timestamp in the actual code, set by the _setTimestamp() method - session.id = 1700000000; - - localStorage.setItem('session', JSON.stringify(session)); - service.isSessionValid().subscribe(isValid => { - expect(isValid).toBeTruthy(); - }); - }); - - it('should return false if session has expired', () => { - const credentialsRes = new CredentialsResponse(); - credentialsRes.message = 'valid'; - - const responseData = ApiResponseData.fromAjaxResponse(new AjaxResponse({} as any, {} as any, {})); - responseData.body = credentialsRes; - - mockApiConnection.v2.auth.checkCredentials.mockReturnValue(of(responseData)); - - // mock Date.now() - jest.spyOn(Date, 'now').mockImplementation(() => Date.parse('2023-06-16')); - localStorage.setItem('session', JSON.stringify(mockSession)); - - service.isSessionValid().subscribe(isValid => { - expect(isValid).toBeFalsy(); - expect(mockApiConnection.v2.auth.checkCredentials).toHaveBeenCalledTimes(1); - }); - }); - }); -}); diff --git a/libs/vre/shared/app-session/src/lib/app-session.ts b/libs/vre/shared/app-session/src/lib/app-session.ts deleted file mode 100644 index 55c5533602..0000000000 --- a/libs/vre/shared/app-session/src/lib/app-session.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { inject, Injectable, signal } from '@angular/core'; -import { toObservable } from '@angular/core/rxjs-interop'; -import { ApiResponseData, ApiResponseError, Constants, CredentialsResponse, UserResponse } from '@dasch-swiss/dsp-js'; - -import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { ApplicationStateService } from '@dasch-swiss/vre/shared/app-state-service'; -import { Observable, of } from 'rxjs'; -import { catchError, map, takeLast } from 'rxjs/operators'; -import { Session } from './session'; - -@Injectable({ - providedIn: 'root', -}) -export class SessionService { - private _applicationStateService = inject(ApplicationStateService); - private _dspApiConnection = inject(DspApiConnectionToken); - - session = signal(undefined); - session$ = toObservable(this.session); - - /** - * max session time in milliseconds - * default value (24h = 24 * 60 * 60 * 1000): 86400000 - * - */ - readonly MAX_SESSION_TIME: number = 3600; - - constructor(private _userApiService: UserApiService) { - // check if the (possibly) existing session is still valid and if not, destroy it - this.isSessionValid() - .pipe(takeLast(1)) - .subscribe(valid => { - if (!valid) { - this.destroySession(); - } - }); - - if (this.session()) { - /** - * set the application state for current/logged-in user - */ - const session = this.session() as Session; - this._userApiService.get(session.user.name as string, 'username').subscribe(response => { - if (response instanceof ApiResponseData) { - this._applicationStateService.set(session.user.name, response.body.user); - } - }); - } - } - - /** - * get session information from localstorage - */ - getSession(): Session | null { - const sessionData = localStorage.getItem('session'); - return sessionData ? JSON.parse(sessionData) : null; - } - - /** - * set session by using the json web token (jwt) and the user object; - * it will be used in the login process - * - * @param jwt Json Web Token - * @param identifier email address or username - * @param type 'email' or 'username' - */ - setSession(jwt: string, identifier: string, type: 'email' | 'username'): Observable { - this._dspApiConnection.v2.jsonWebToken = jwt || ''; - - // get user information - return this._userApiService.get(identifier, type).pipe( - map(response => { - this._storeSessionInLocalStorage(response, jwt); - // return type is void - return true; - }) - ); - } - - /** - * validate intern session and check knora api credentials if necessary. - * If a json web token exists, it doesn't mean that the knora api credentials are still valid. - * - */ - isSessionValid(): Observable { - // mix of checks with session.validation and this.authenticate - const sessionData = localStorage.getItem('session'); - - if (sessionData) { - const session = JSON.parse(sessionData); - const tsNow: number = this._setTimestamp(); - this._dspApiConnection.v2.jsonWebToken = session.user.jwt; - - // check if the session is still valid: - if (session.id + this.MAX_SESSION_TIME <= tsNow) { - // the internal session has expired - // check if the api credentials are still valid - - return this._dspApiConnection.v2.auth.checkCredentials().pipe( - map((credentials: ApiResponseData | ApiResponseError) => - this._updateSessionId(credentials, session, tsNow) - ), - catchError(() => { - // if there is any error checking the credentials (mostly a 401 for after - // switching the server where this session/the credentials are unknown), we destroy the session - // so a new login is required - this.destroySession(); - return of(false); - }) - ); - } else { - // the internal session is still valid - this.session.set(session); - return of(true); - } - } else { - // no session found; update knora api connection with empty jwt - this._dspApiConnection.v2.jsonWebToken = ''; - return of(false); - } - } - - /** - * destroy session by removing the session from local storage - * - */ - destroySession() { - localStorage.removeItem('session'); - this.session.set(undefined); - } - - /** - * returns a timestamp represented in seconds - * - */ - private _setTimestamp(): number { - return Math.floor(Date.now() / 1000); - } - - /** - * store session in local storage - * @param response response from getUser method call - * @param jwt JSON web token string - */ - private _storeSessionInLocalStorage(response: UserResponse, jwt: string) { - let sysAdmin = false; - const projectAdmin: string[] = []; - - // get permission information: a) is user sysadmin? b) get list of project iri's where user is project admin - const groupsPerProject = response.user.permissions.groupsPerProject; - - if (groupsPerProject) { - const groupsPerProjectKeys: string[] = Object.keys(groupsPerProject); - - for (const key of groupsPerProjectKeys) { - if (key === Constants.SystemProjectIRI) { - sysAdmin = groupsPerProject[key].indexOf(Constants.SystemAdminGroupIRI) > -1; - } - - if (groupsPerProject[key].indexOf(Constants.ProjectAdminGroupIRI) > -1) { - projectAdmin.push(key); - } - } - } - - // store session information in browser's localstorage - const session = { - id: this._setTimestamp(), - user: { - name: response.user.username, - jwt, - lang: response.user.lang, - sysAdmin, - projectAdmin, - }, - }; - - // update localStorage - localStorage.setItem('session', JSON.stringify(session)); - this.session.set(session); - } - - /** - * updates the id of the current session in the local storage - * @param credentials response from getCredentials method call - * @param session the current session - * @param timestamp timestamp in form of a number - */ - private _updateSessionId( - credentials: ApiResponseData | ApiResponseError, - session: Session, - timestamp: number - ): boolean { - if (credentials instanceof ApiResponseData) { - // the dsp api credentials are still valid - // update the session.id - session.id = timestamp; - localStorage.setItem('session', JSON.stringify(session)); - this.session.set(session); - return true; - } else { - // a user is not authenticated anymore! - this.destroySession(); - return false; - } - } -} diff --git a/libs/vre/shared/app-session/src/lib/auth.service.ts b/libs/vre/shared/app-session/src/lib/auth.service.ts index d308b25bd1..f9be309236 100644 --- a/libs/vre/shared/app-session/src/lib/auth.service.ts +++ b/libs/vre/shared/app-session/src/lib/auth.service.ts @@ -1,198 +1,88 @@ -import { EventEmitter, Injectable, Output, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { ApiResponseData, ApiResponseError, CredentialsResponse, LoginResponse, User } from '@dasch-swiss/dsp-js'; -import { Auth, DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; +import { Inject, Injectable } from '@angular/core'; + +import { ApiResponseData, ApiResponseError, KnoraApiConnection, LoginResponse } from '@dasch-swiss/dsp-js'; +import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { ClearListsAction, ClearOntologiesAction, ClearOntologyClassAction, ClearProjectsAction, + LoadUserAction, LogUserOutAction, - UserSelectors, } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, Store, ofActionSuccessful } from '@ngxs/store'; -import jwt_decode, { JwtPayload } from 'jwt-decode'; -import { Observable, of, throwError } from 'rxjs'; -import { catchError, map, switchMap, take, takeLast, tap } from 'rxjs/operators'; -import { LoginError, ServerError } from './error'; +import { Store } from '@ngxs/store'; +import { of, throwError } from 'rxjs'; +import { catchError, map, switchMap, take, tap } from 'rxjs/operators'; +import { AccessTokenService } from './access-token.service'; +import { LoginError } from './error'; @Injectable({ providedIn: 'root' }) export class AuthService { - private tokenRefreshIntervalId: any; - private _dspApiConnection = inject(DspApiConnectionToken); - - get tokenUser() { - return this.getTokenUser(); - } - - @Output() loginSuccessfulEvent = new EventEmitter(); - constructor( private store: Store, - private _actions: Actions, - private router: Router // private intervalWrapper: IntervalWrapperService - ) { - // check if the (possibly) existing session is still valid and if not, destroy it - this.isSessionValid$(); - - // if (this.isLoggedIn()) { - // this.startTokenRefresh(); - // } - } - - /** - * validate intern session and check knora api credentials if necessary. - * If a json web token exists, it doesn't mean that the knora api credentials are still valid. - * - */ - isSessionValid$(forceLogout: boolean = false): Observable { - // mix of checks with session.validation and this.authenticate - const accessToken = this.getAccessToken(); - if (accessToken) { - this._dspApiConnection.v2.jsonWebToken = accessToken; - - // check if the session is still valid: - if (this.isTokenExpired(accessToken)) { - // the internal session has expired - // check if the api credentials are still valid - return this._dspApiConnection.v2.auth.checkCredentials().pipe( - map((credentials: ApiResponseData | ApiResponseError) => - this._updateSessionId(credentials) - ), - catchError(() => { - // if there is any error checking the credentials (mostly a 401 for after - // switching the server where this session/the credentials are unknown), we destroy the session - // so a new login is required - this.doLogoutUser(); - return of(false); - }) - ); - } else { - // the internal session is still valid - return of(true); - } - } else { - // no session found; update knora api connection with empty jwt - this._dspApiConnection.v2.jsonWebToken = ''; - - const username = this.store.selectSnapshot(UserSelectors.username); - if (username) { - this.clearState(); - } - - if (forceLogout) { - this.doLogoutUser(); - } - - return of(false); - } - } - - /** - * updates the id of the current session in the local storage - * @param credentials response from getCredentials method call - * @param session the current session - * @param timestamp timestamp in form of a number - */ - private _updateSessionId(credentials: ApiResponseData | ApiResponseError): boolean { - if (credentials instanceof ApiResponseData) { - // the dsp api credentials are still valid - this.storeToken(credentials.body.message); - return true; - } else { - // a user is not authenticated anymore! - this.doLogoutUser(); - return false; - } + private _accessTokenService: AccessTokenService, + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection + ) {} + + isCredentialsValid$() { + return this._dspApiConnection.v2.auth.checkCredentials().pipe( + take(1), + map(response => { + if (response instanceof ApiResponseError) { + throwError(response); + } + return true; + }), + catchError(() => { + return of(false); + }) + ); } /** * Login user * @param identifier can be the email or the username * @param password the password of the user - * @returns an Either with the session or an error message */ - apiLogin$(identifier: string, password: string): Observable { - const identifierType: 'iri' | 'email' | 'username' = identifier.indexOf('@') > -1 ? 'email' : 'username'; + login$(identifier: string, password: string) { + const identifierType = identifier.indexOf('@') > -1 ? 'email' : 'username'; return this._dspApiConnection.v2.auth.login(identifierType, identifier, password).pipe( - takeLast(1), - tap((response: ApiResponseData | ApiResponseError) => { - if (response instanceof ApiResponseData) { - this.storeToken(response.body.token); - } - }), - switchMap((response: ApiResponseData | ApiResponseError) => { - if (response instanceof ApiResponseData) { - return of(true); - } else if (response.status === 401 || response.status === 403) { - // wrong credentials - return throwError({ + tap(response => { + // wrong credentials + if (response.status === 401 || response.status === 403) { + throwError({ type: 'login', status: response.status, msg: 'Wrong credentials', }); - } else { - // server error - return throwError({ - type: 'server', - status: response.status, - msg: 'Server error', - }); } - }) - ); - } - // TODO refresh access token using API - refreshToken$(): Observable { - const refreshToken = this.getRefreshToken(); - if (!refreshToken) { - return of(false); - } + if (response instanceof ApiResponseError) { + throwError(response); + } - return of(false); + const encodedJWT = (response as ApiResponseData).body.token; + this._accessTokenService.storeToken(encodedJWT); + this._dspApiConnection.v2.jsonWebToken = encodedJWT; + }), + switchMap(() => this.store.dispatch(new LoadUserAction(identifier))) + ); } logout() { - // TODO ? logout by access token missing ? this._dspApiConnection.v2.auth .logout() - .pipe( - take(1), - catchError((error: ApiResponseError) => of(error?.status === 200)) - ) - .subscribe((response: any) => { - if (!(response instanceof ApiResponseData)) { - throwError({ - type: 'server', - status: response.status, - msg: 'Logout was not successful', - }); - return; - } - - if (response.body.status === 0) { - this.doLogoutUser(); - } + .pipe(switchMap(() => this.clearState())) + .subscribe(() => { + this._accessTokenService.removeTokens(); + this._dspApiConnection.v2.jsonWebToken = ''; + window.location.reload(); }); } - doLogoutUser() { - this.removeTokens(); - this._actions - .pipe(ofActionSuccessful(ClearProjectsAction)) - .pipe(take(1)) - .subscribe(() => - this.router - .navigate([RouteConstants.logout], { skipLocationChange: true }) - .then(() => this.router.navigate([RouteConstants.home])) - ); - this.clearState(); - clearTimeout(this.tokenRefreshIntervalId); - } - - clearState() { - this.store.dispatch([ + private clearState() { + return this.store.dispatch([ new LogUserOutAction(), new ClearProjectsAction(), new ClearListsAction(), @@ -200,102 +90,4 @@ export class AuthService { new ClearOntologyClassAction(), ]); } - - isLoggedIn() { - return !!this.getAccessToken(); - } - - getAccessToken() { - return localStorage.getItem(Auth.AccessToken); - } - - getRefreshToken() { - return localStorage.getItem(Auth.Refresh_token); - } - - private storeToken(token: string) { - localStorage.setItem(Auth.AccessToken, token); - // localStorage.setItem(this.REFRESH_TOKEN, token); - this.startTokenRefresh(); - } - - private refreshAccessToken(access_token: string) { - localStorage.setItem(Auth.AccessToken, access_token); - this.startTokenRefresh(); - } - - private removeTokens() { - localStorage.removeItem(Auth.AccessToken); - localStorage.removeItem(Auth.Refresh_token); - } - - isTokenExpired(token?: string | null): boolean { - if (!token) { - token = this.getAccessToken(); - } - - if (!token) { - return true; - } - - const date = this.getTokenExpirationDate(token); - if (date == null) { - return false; - } - - return date.setSeconds(date.getSeconds() - 30).valueOf() <= new Date().valueOf(); - } - - getTokenExpirationDate(token: string): Date | null { - const decoded = jwt_decode(token); - - if (decoded.exp === undefined) { - return null; - } - - const date = new Date(0); - date.setUTCSeconds(decoded.exp); - - return date; - } - - getTokenExp(token: string): number { - const decoded = jwt_decode(token); - - if (decoded.exp === undefined) { - return 0; - } - - return decoded.exp; - } - - startTokenRefresh() { - const token = this.getAccessToken(); - - if (!token) { - return; - } - - const exp = this.getTokenExp(token); - const date = new Date(0); - date.setUTCSeconds(exp); - - if (this.tokenRefreshIntervalId) { - clearInterval(this.tokenRefreshIntervalId); - } - } - - private getTokenUser(): string { - const token = this.getAccessToken(); - if (!token) { - return ''; - } - - const decoded = jwt_decode(token); - if (decoded.sub === undefined) { - return ''; - } - - return decoded.sub; - } } diff --git a/libs/vre/shared/app-session/src/lib/auto-login.service.ts b/libs/vre/shared/app-session/src/lib/auto-login.service.ts new file mode 100644 index 0000000000..47ef789e17 --- /dev/null +++ b/libs/vre/shared/app-session/src/lib/auto-login.service.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@angular/core'; +import { KnoraApiConnection } from '@dasch-swiss/dsp-js'; +import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; +import { LoadUserAction } from '@dasch-swiss/vre/shared/app-state'; +import { Store } from '@ngxs/store'; +import { BehaviorSubject, throwError } from 'rxjs'; +import { finalize, switchMap } from 'rxjs/operators'; +import { AccessTokenService } from './access-token.service'; +import { AuthService } from './auth.service'; + +@Injectable({ providedIn: 'root' }) +export class AutoLoginService { + hasCheckedCredentials$ = new BehaviorSubject(false); + + constructor( + private _accessTokenService: AccessTokenService, + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _store: Store, + private _authService: AuthService + ) {} + + setup() { + const encodedJWT = this._accessTokenService.getTokenUser(); + if (!encodedJWT) { + this.hasCheckedCredentials$.next(true); + return; + } + + const decodedToken = this._accessTokenService.decodedAccessToken(encodedJWT); + if (!decodedToken || this._accessTokenService.isValidToken(decodedToken)) { + this.hasCheckedCredentials$.next(true); + this._accessTokenService.removeTokens(); + return; + } + + this._dspApiConnection.v2.jsonWebToken = encodedJWT; + + this._authService + .isCredentialsValid$() + .pipe( + switchMap(isValid => { + if (!isValid) { + throwError('Credentials not valid'); + } + + const userIri = decodedToken.sub; + if (!userIri) { + return throwError('Decoded user in JWT token is not valid.'); + } + + return this._store.dispatch(new LoadUserAction(userIri, 'iri')); + }), + finalize(() => this.hasCheckedCredentials$.next(true)) + ) + .subscribe({ + error: () => { + this._accessTokenService.removeTokens(); + this._dspApiConnection.v2.jsonWebToken = ''; + }, + }); + } +} diff --git a/libs/vre/shared/app-session/src/lib/local-storage-watcher.service.ts b/libs/vre/shared/app-session/src/lib/local-storage-watcher.service.ts new file mode 100644 index 0000000000..7bab3de3aa --- /dev/null +++ b/libs/vre/shared/app-session/src/lib/local-storage-watcher.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { fromEvent } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class LocalStorageWatcherService { + watchAccessToken() { + fromEvent(window, 'storage') + .pipe( + takeUntilDestroyed(), + filter(event => { + return event.key === 'ACCESS_TOKEN' && event.oldValue !== event.newValue; + }) + ) + .subscribe(() => { + window.location.reload(); + }); + } +} diff --git a/libs/vre/shared/app-state-service/.eslintrc.json b/libs/vre/shared/app-state-service/.eslintrc.json deleted file mode 100644 index 6ebf70478b..0000000000 --- a/libs/vre/shared/app-state-service/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["../../../../.eslintrc-angular.json"] -} diff --git a/libs/vre/shared/app-state-service/README.md b/libs/vre/shared/app-state-service/README.md deleted file mode 100644 index 787937ae2e..0000000000 --- a/libs/vre/shared/app-state-service/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# app-state-service - -This library handles the state of the application. It works via a dictionary that stores a key and a `StateContent` object. - -The value of the `StateContent` object can be one of the following: -- `ReadUser` -- `ReadUser[]` -- `ReadProject` -- `ReadOntology` -- `ReadOntology[]` -- `ReadGroup[]` -- `ListNodeInfo[]` - -## Usage -To use the library, add it to the imports of your library: - - import { ApplicationStateService } from '@dasch-swiss/vre/shared/app-state-service'; - -Then add it to the component via dependecy injection: - - constructor( - private _applicationStateService: ApplicationStateService, - ) {} - -Normally you get the value for the `StateContent` object from a response from the DSP-API but you can also create your own object if need be. - -## Example -Here is an example of making a request to the DSP-API to get all of the groups of which the response is an array of type `ReadGroup`. - - this._dspApiConnection.admin.groupsEndpoint.getGroups().subscribe( - (response: ApiResponseData) => - this._applicationStateService.set('groups_of_' + this.projectCode, response.body.groups) - ); -## Running unit tests - -Run `nx test app-state-service` to execute the unit tests. diff --git a/libs/vre/shared/app-state-service/jest.config.ts b/libs/vre/shared/app-state-service/jest.config.ts deleted file mode 100644 index 0567119a14..0000000000 --- a/libs/vre/shared/app-state-service/jest.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'app-state-service', - preset: '../../../../jest.preset.js', - setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../../../../coverage/libs/vre/shared/app-state-service', - transform: { - '^.+\\.(ts|mjs|js|html)$': [ - 'jest-preset-angular', - { - tsconfig: '/tsconfig.spec.json', - stringifyContentPathRegex: '\\.(html|svg)$', - }, - ], - }, - transformIgnorePatterns: ['node_modules/(?!@angular|@dasch-swiss)'], - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], -}; diff --git a/libs/vre/shared/app-state-service/project.json b/libs/vre/shared/app-state-service/project.json deleted file mode 100644 index f4399efb62..0000000000 --- a/libs/vre/shared/app-state-service/project.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "app-state-service", - "$schema": "../../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/vre/shared/app-state-service/src", - "prefix": "dasch-swiss", - "tags": [], - "projectType": "library", - "targets": { - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "libs/vre/shared/app-state-service/jest.config.ts", - "passWithNoTests": true - }, - "configurations": { - "ci": { - "ci": true, - "codeCoverage": true - } - } - }, - "lint": { - "executor": "@nx/linter:eslint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": [ - "libs/vre/shared/app-state-service/**/*.ts", - "libs/vre/shared/app-state-service/**/*.html" - ] - } - } - } -} diff --git a/libs/vre/shared/app-state-service/src/index.ts b/libs/vre/shared/app-state-service/src/index.ts deleted file mode 100644 index abd9078e1b..0000000000 --- a/libs/vre/shared/app-state-service/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/app-state.service'; diff --git a/libs/vre/shared/app-state-service/src/lib/app-state.service.spec.ts b/libs/vre/shared/app-state-service/src/lib/app-state.service.spec.ts deleted file mode 100644 index 4e37de3dcf..0000000000 --- a/libs/vre/shared/app-state-service/src/lib/app-state.service.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { ReadUser } from '@dasch-swiss/dsp-js'; -import { ApplicationStateService } from './app-state.service'; - -describe('ApplicationStateService', () => { - let service: ApplicationStateService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - }); - - service = TestBed.inject(ApplicationStateService); - expect(service).toBeTruthy(); - - // reset the state before each test - service.destroy(); - }); - - it('should set a value in the dictionary', () => { - const user = new ReadUser(); - service.set('user', user); - expect(service.has('user')).toBeTruthy(); - }); - - it('should get a value in the dictionary', () => { - const user = new ReadUser(); - user.id = '1234'; - - service.set('user', user); - - const result$ = service.get('user'); - - result$.subscribe(result => { - expect(result).toMatchObject(user); - }); - }); - - it('should throw an error if the key is not found', () => { - const result$ = service.get('user_not_found'); - - result$.subscribe( - // eslint-disable-next-line @typescript-eslint/no-empty-function - () => {}, - error => { - expect(error).toBeTruthy(); - } - ); - }); - - it('should delete a value in the dictionary', () => { - const user = new ReadUser(); - user.id = '1234'; - service.set('user', user); - - const user2 = new ReadUser(); - user2.id = '5678'; - service.set('user2', user2); - - service.delete('user'); - - expect(service.has('user')).toBeFalsy(); - expect(service.has('user2')).toBeTruthy(); - }); - - it('should destroy the dictionary', () => { - const user = new ReadUser(); - user.id = '1234'; - service.set('user', user); - - const user2 = new ReadUser(); - user2.id = '5678'; - service.set('user2', user2); - - service.destroy(); - - expect(service.has('user')).toBeFalsy(); - expect(service.has('user2')).toBeFalsy(); - }); -}); diff --git a/libs/vre/shared/app-state-service/src/lib/app-state.service.ts b/libs/vre/shared/app-state-service/src/lib/app-state.service.ts deleted file mode 100644 index a6021b6cd9..0000000000 --- a/libs/vre/shared/app-state-service/src/lib/app-state.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ReadUser, ReadProject, ReadOntology, ReadGroup, ListNodeInfo } from '@dasch-swiss/dsp-js'; -import { Observable, of, throwError } from 'rxjs'; - -interface StateContent { - value: ReadUser | ReadUser[] | ReadProject | ReadOntology | ReadOntology[] | ReadGroup[] | ListNodeInfo[]; -} - -@Injectable({ - providedIn: 'root', -}) -/** - * Application State Service is an observables based in-memory state implementation - * Used to keep track of the state of the application - * @export - * @class ApplicationStateService - */ -export class ApplicationStateService { - private _applicationState: Map = new Map(); - - /** - * gets the value from a state if the key is provided - * @param key Key is the id of the content - */ - get( - key: string - ): Observable { - if (this.has(key)) { - const content = this._applicationState.get(key); - // content should never be undefined but we'll check anyway - // if it is undefined, it means the app is in an invalid state - if (content === undefined) { - return throwError(`Requested key "${key}" has value of undefined in the application state`); - } else { - return of(content.value); - } - } else { - return throwError(`Requested key "${key}" is not available in the application state`); - } - } - - /** - * sets the value with key in the application state - * @param key Key is the id of the content - * @param value Value is the content - */ - set( - key: string, - value: ReadUser | ReadUser[] | ReadProject | ReadOntology | ReadOntology[] | ReadGroup[] | ListNodeInfo[] - ): void { - this._applicationState.set(key, { value }); - } - - /** - * checks if the key exists in the application state - * @param key Key is the id of the content - */ - has(key: string): boolean { - return this._applicationState.has(key); - } - - /** - * delete a states content by key - * @param key Key is the id of the content - */ - delete(key: string) { - this._applicationState.delete(key); - } - - /** - * clear the whole application state - */ - destroy() { - this._applicationState.clear(); - } -} diff --git a/libs/vre/shared/app-state-service/src/test-setup.ts b/libs/vre/shared/app-state-service/src/test-setup.ts deleted file mode 100644 index ab1eeeb335..0000000000 --- a/libs/vre/shared/app-state-service/src/test-setup.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment -globalThis.ngJest = { - testEnvironmentOptions: { - errorOnUnknownElements: true, - errorOnUnknownProperties: true, - }, -}; -import 'jest-preset-angular/setup-jest'; diff --git a/libs/vre/shared/app-state-service/tsconfig.json b/libs/vre/shared/app-state-service/tsconfig.json deleted file mode 100644 index b9e5be0863..0000000000 --- a/libs/vre/shared/app-state-service/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - "target": "es2022", - "useDefineForClassFields": false, - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ], - "extends": "../../../../tsconfig.base.json", - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - } -} diff --git a/libs/vre/shared/app-state-service/tsconfig.lib.json b/libs/vre/shared/app-state-service/tsconfig.lib.json deleted file mode 100644 index 9127387056..0000000000 --- a/libs/vre/shared/app-state-service/tsconfig.lib.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../../dist/out-tsc", - "declaration": true, - "declarationMap": true, - "inlineSources": true, - "types": [] - }, - "exclude": [ - "src/**/*.spec.ts", - "src/test-setup.ts", - "jest.config.ts", - "src/**/*.test.ts" - ], - "include": ["src/**/*.ts"] -} diff --git a/libs/vre/shared/app-state-service/tsconfig.spec.json b/libs/vre/shared/app-state-service/tsconfig.spec.json deleted file mode 100644 index 6e5925e5c4..0000000000 --- a/libs/vre/shared/app-state-service/tsconfig.spec.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../../dist/out-tsc", - "module": "commonjs", - "target": "es2016", - "types": ["jest", "node"] - }, - "files": ["src/test-setup.ts"], - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/libs/vre/shared/app-state/src/lib/lists/lists.state.ts b/libs/vre/shared/app-state/src/lib/lists/lists.state.ts index d8b65547a5..d8c79687b1 100644 --- a/libs/vre/shared/app-state/src/lib/lists/lists.state.ts +++ b/libs/vre/shared/app-state/src/lib/lists/lists.state.ts @@ -1,7 +1,5 @@ import { Injectable } from '@angular/core'; -import { ApiResponseError } from '@dasch-swiss/dsp-js'; import { ListApiService } from '@dasch-swiss/vre/shared/app-api'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { Action, State, StateContext } from '@ngxs/store'; import { of } from 'rxjs'; import { finalize, map, take, tap } from 'rxjs/operators'; @@ -19,10 +17,7 @@ const defaults: ListsStateModel = { }) @Injectable() export class ListsState { - constructor( - private _listApiService: ListApiService, - private _errorHandler: AppErrorHandler - ) {} + constructor(private _listApiService: ListApiService) {} @Action(LoadListsInProjectAction) loadListsInProject(ctx: StateContext, { projectIri }: LoadListsInProjectAction) { @@ -43,9 +38,6 @@ export class ListsState { next: () => { ctx.patchState({ isLoading: false }); }, - error: (error: ApiResponseError) => { - this.handleDeleteError(error); - }, }) ); } @@ -59,11 +51,4 @@ export class ListsState { }) ); } - - private handleDeleteError(error: ApiResponseError): void { - // if DSP-API returns a 400, it is likely that the list node is in use so we inform the user of this - if (error.status !== 400) { - this._errorHandler.showMessage(error); - } - } } diff --git a/libs/vre/shared/app-state/src/lib/ontologies/ontologies.state.ts b/libs/vre/shared/app-state/src/lib/ontologies/ontologies.state.ts index 2da045d6c7..c646c2364f 100644 --- a/libs/vre/shared/app-state/src/lib/ontologies/ontologies.state.ts +++ b/libs/vre/shared/app-state/src/lib/ontologies/ontologies.state.ts @@ -15,10 +15,9 @@ import { } from '@dasch-swiss/dsp-js'; import { getAllEntityDefinitionsAsArray } from '@dasch-swiss/vre/shared/app-api'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { OntologyClassService, ProjectService, SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; -import { Action, Actions, State, StateContext, ofActionSuccessful } from '@ngxs/store'; +import { Action, Actions, ofActionSuccessful, State, StateContext } from '@ngxs/store'; import { of } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; import { LoadListsInProjectAction } from '../lists/lists.actions'; @@ -60,7 +59,6 @@ export class OntologiesState { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, private _sortingService: SortingService, private _projectService: ProjectService, private _actions$: Actions, @@ -162,7 +160,6 @@ export class OntologiesState { }, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); }, }) ); @@ -207,7 +204,6 @@ export class OntologiesState { }, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); }, }) ); @@ -229,7 +225,6 @@ export class OntologiesState { }, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true }); - this._errorHandler.showMessage(error); }, }) ); @@ -332,7 +327,6 @@ export class OntologiesState { }, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); }, }) ); @@ -375,7 +369,6 @@ export class OntologiesState { next: () => {}, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); }, }) ); @@ -402,7 +395,6 @@ export class OntologiesState { }, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true }); - this._errorHandler.showMessage(error); }, }) ); diff --git a/libs/vre/shared/app-state/src/lib/projects/projects.selectors.ts b/libs/vre/shared/app-state/src/lib/projects/projects.selectors.ts index 1f99c3ebe8..b5ecb727d5 100644 --- a/libs/vre/shared/app-state/src/lib/projects/projects.selectors.ts +++ b/libs/vre/shared/app-state/src/lib/projects/projects.selectors.ts @@ -23,6 +23,11 @@ export class ProjectsSelectors { return state.allProjects; } + @Selector([ProjectsState]) + static allProjectShortcodes(state: ProjectsStateModel): string[] { + return state.allProjects.map(project => project.shortcode); + } + @Selector([ProjectsState]) static isProjectsLoading(state: ProjectsStateModel): boolean { return state.isLoading; diff --git a/libs/vre/shared/app-state/src/lib/projects/projects.state.ts b/libs/vre/shared/app-state/src/lib/projects/projects.state.ts index 79b322c596..ed3a0e7126 100644 --- a/libs/vre/shared/app-state/src/lib/projects/projects.state.ts +++ b/libs/vre/shared/app-state/src/lib/projects/projects.state.ts @@ -75,7 +75,6 @@ export class ProjectsState { }, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); }, }), finalize(() => { @@ -121,7 +120,6 @@ export class ProjectsState { }, error: (error: ApiResponseError) => { ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); }, }), concatMap(() => { @@ -176,9 +174,6 @@ export class ProjectsState { ctx.dispatch([new SetUserAction(response.body.user), new LoadProjectMembersAction(projectIri)]); ctx.patchState({ isMembershipLoading: false }); }, - error: error => { - this.errorHandler.showMessage(error); - }, }) ); } @@ -199,7 +194,6 @@ export class ProjectsState { }, error: error => { ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); }, }) ); @@ -231,7 +225,6 @@ export class ProjectsState { }, error: error => { ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); }, }) ); @@ -264,9 +257,6 @@ export class ProjectsState { projectGroups: groups, }); }, - error: error => { - this.errorHandler.showMessage(error); - }, }) ); } @@ -279,6 +269,12 @@ export class ProjectsState { next: response => { ctx.dispatch(new LoadProjectsAction()); }, + error: () => { + ctx.patchState({ hasLoadingErrors: true }); + }, + }), + finalize(() => { + ctx.patchState({ isLoading: false }); }) ); } diff --git a/libs/vre/shared/app-state/src/lib/user/user.actions.ts b/libs/vre/shared/app-state/src/lib/user/user.actions.ts index 1485d2e3d4..f8e10646cb 100644 --- a/libs/vre/shared/app-state/src/lib/user/user.actions.ts +++ b/libs/vre/shared/app-state/src/lib/user/user.actions.ts @@ -2,7 +2,10 @@ import { ReadUser, User } from '@dasch-swiss/dsp-js'; export class LoadUserAction { static readonly type = '[User] Load User'; - constructor(public username: string) {} + constructor( + public identifier: string, + public idType: 'iri' | 'username' = 'username' + ) {} } export class LoadUserContentByIriAction { diff --git a/libs/vre/shared/app-state/src/lib/user/user.selectors.ts b/libs/vre/shared/app-state/src/lib/user/user.selectors.ts index 17d6269062..87ea60b535 100644 --- a/libs/vre/shared/app-state/src/lib/user/user.selectors.ts +++ b/libs/vre/shared/app-state/src/lib/user/user.selectors.ts @@ -18,23 +18,21 @@ export class UserSelectors { @Selector([UserState]) static activeUsers(state: UserStateModel): ReadUser[] { - return state.allUsers.filter((user: ReadUser) => user.status === true); + return state.allUsers.filter((user: ReadUser) => user.status); } @Selector([UserState]) static inactiveUsers(state: UserStateModel): ReadUser[] { - return state.allUsers.filter((user: ReadUser) => user.status !== true); + return state.allUsers.filter((user: ReadUser) => !user.status); } @Selector([UserState]) - static isLoggedIn(state: UserStateModel): boolean { - return ( - !state.isLoading && !!localStorage.getItem(Auth.AccessToken) && state.user !== null && state.user?.username !== '' - ); + static isLoggedIn(state: UserStateModel) { + return !state.isLoading && state.user !== null; } @Selector([UserState]) - static user(state: UserStateModel): User | ReadUser | null | undefined { + static user(state: UserStateModel): ReadUser | null { return state.user; } diff --git a/libs/vre/shared/app-state/src/lib/user/user.state-model.ts b/libs/vre/shared/app-state/src/lib/user/user.state-model.ts index 8aee1eaa05..4f6d3b1bc6 100644 --- a/libs/vre/shared/app-state/src/lib/user/user.state-model.ts +++ b/libs/vre/shared/app-state/src/lib/user/user.state-model.ts @@ -1,8 +1,8 @@ import { ReadUser, User } from '@dasch-swiss/dsp-js'; export class UserStateModel { - isLoading: boolean | undefined; - user: User | ReadUser | null | undefined; + isLoading = false; + user: ReadUser | null = null; userProjectAdminGroups: string[] = []; // before was projectAdmin isMemberOfSystemAdminGroup = false; // before was sysAdmin allUsers: ReadUser[] = []; diff --git a/libs/vre/shared/app-state/src/lib/user/user.state.ts b/libs/vre/shared/app-state/src/lib/user/user.state.ts index 84c4f81754..f8114dfe0d 100644 --- a/libs/vre/shared/app-state/src/lib/user/user.state.ts +++ b/libs/vre/shared/app-state/src/lib/user/user.state.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; -import { ApiResponseError, Constants, ReadUser } from '@dasch-swiss/dsp-js'; +import { Constants, ReadUser } from '@dasch-swiss/dsp-js'; import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { Action, State, StateContext } from '@ngxs/store'; import { of } from 'rxjs'; import { map, take, tap } from 'rxjs/operators'; @@ -33,15 +32,12 @@ const defaults = { }) @Injectable() export class UserState { - constructor( - private _userApiService: UserApiService, - private _errorHandler: AppErrorHandler - ) {} + constructor(private _userApiService: UserApiService) {} @Action(LoadUserAction) - loadUser(ctx: StateContext, { username }: LoadUserAction) { + loadUser(ctx: StateContext, { identifier, idType }: LoadUserAction) { ctx.patchState({ isLoading: true }); - return this._userApiService.get(username, 'username').pipe( + return this._userApiService.get(identifier, idType).pipe( take(1), map(response => { ctx.setState({ @@ -73,9 +69,6 @@ export class UserState { isLoading: false, }); }, - error: (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - }, }) ); } @@ -144,12 +137,7 @@ export class UserState { @Action(LogUserOutAction) logUserOut(ctx: StateContext) { - return of(ctx.getState()).pipe( - map(currentState => { - ctx.setState(defaults); - return currentState; - }) - ); + ctx.setState(defaults); } @Action(LoadUsersAction) @@ -168,9 +156,6 @@ export class UserState { response.users.map(u => ctx.dispatch(new LoadUserContentByIriAction(u.id))); } }, - error: error => { - this._errorHandler.showMessage(error); - }, }) ); } @@ -192,9 +177,6 @@ export class UserState { state.isLoading = false; ctx.patchState(state); }, - error: error => { - this._errorHandler.showMessage(error); - }, }) ); } diff --git a/libs/vre/shared/app-string-literal/src/lib/app-string-literal/dasch-swiss-string-literal.component.ts b/libs/vre/shared/app-string-literal/src/lib/app-string-literal/dasch-swiss-string-literal.component.ts index 6362e3d0a0..f8dd09aba4 100644 --- a/libs/vre/shared/app-string-literal/src/lib/app-string-literal/dasch-swiss-string-literal.component.ts +++ b/libs/vre/shared/app-string-literal/src/lib/app-string-literal/dasch-swiss-string-literal.component.ts @@ -24,7 +24,6 @@ import { MatInputModule } from '@angular/material/input'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; import { StringLiteral } from '@dasch-swiss/dsp-js'; import { NgxsStoreModule, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; -import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { Store } from '@ngxs/store'; @Component({ @@ -40,7 +39,6 @@ import { Store } from '@ngxs/store'; FormsModule, ReactiveFormsModule, NgxsStoreModule, - NgxsStoragePluginModule, ], templateUrl: './dasch-swiss-string-literal.component.html', styleUrls: ['./dasch-swiss-string-literal.component.scss'], diff --git a/libs/vre/shared/app-string-literal/src/lib/human-readable-error.pipe.ts b/libs/vre/shared/app-string-literal/src/lib/human-readable-error.pipe.ts index 2a6ea4e0fa..99ce3ca6eb 100644 --- a/libs/vre/shared/app-string-literal/src/lib/human-readable-error.pipe.ts +++ b/libs/vre/shared/app-string-literal/src/lib/human-readable-error.pipe.ts @@ -17,7 +17,7 @@ export class HumanReadableErrorPipe implements PipeTransform { } if (error.hasOwnProperty('minlength')) { - return `Must be greater than or equal to ${ + return `The length must be greater than or equal to ${ ( error['minlength'] as { requiredLength: number; @@ -27,7 +27,9 @@ export class HumanReadableErrorPipe implements PipeTransform { } if (error.hasOwnProperty('maxlength')) { - return `Must be less than or equal to ${(error['maxlength'] as { requiredLength: number }).requiredLength}`; + return `The length must be less than or equal to ${ + (error['maxlength'] as { requiredLength: number }).requiredLength + }`; } if (error.hasOwnProperty('existingName')) { diff --git a/libs/vre/shared/app-string-literal/src/lib/multi-language-form.service.ts b/libs/vre/shared/app-string-literal/src/lib/multi-language-form.service.ts index 7f2b76d78c..c00b2095a5 100644 --- a/libs/vre/shared/app-string-literal/src/lib/multi-language-form.service.ts +++ b/libs/vre/shared/app-string-literal/src/lib/multi-language-form.service.ts @@ -53,7 +53,13 @@ export class MultiLanguageFormService { return this.formArray.controls.find(control => control.value.language === lang && control.value.value !== ''); } - changeLanguage(languageIndex: number) { + changeLanguage(languageIndex: number, deleteCurrentIfEmpty = true) { + const currentValidators = (this.formArray.controls[0].get('value') as FormControl).validator; + + if (deleteCurrentIfEmpty && this.selectedFormControl.value.length === 0) { + this.formArray.removeAt(this.formArray.controls.indexOf(this.selectedFormControl)); + } + const language = this.availableLanguages[languageIndex]; const languageFoundIndex = this.formArray.value.findIndex(array => array.language === language); @@ -61,10 +67,10 @@ export class MultiLanguageFormService { this.formArray.push( this._fb.group({ language, - value: ['', (this.formArray.controls[0].get('value') as FormControl).validator], + value: ['', currentValidators], }) ); - this.changeLanguage(languageIndex); + this.changeLanguage(languageIndex, false); } else { this.selectedLanguageIndex = languageIndex; } diff --git a/libs/vre/shared/app-string-literal/src/lib/multi-language-input.component.ts b/libs/vre/shared/app-string-literal/src/lib/multi-language-input.component.ts index 8fd423ea48..18b045ae43 100644 --- a/libs/vre/shared/app-string-literal/src/lib/multi-language-input.component.ts +++ b/libs/vre/shared/app-string-literal/src/lib/multi-language-input.component.ts @@ -7,7 +7,6 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { NgxsStoreModule } from '@dasch-swiss/vre/shared/app-state'; -import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { HumanReadableErrorPipe } from './human-readable-error.pipe'; import { MultiLanguageFormService } from './multi-language-form.service'; @@ -25,7 +24,6 @@ import { MultiLanguageFormService } from './multi-language-form.service'; FormsModule, ReactiveFormsModule, NgxsStoreModule, - NgxsStoragePluginModule, HumanReadableErrorPipe, ], template: ` diff --git a/libs/vre/shared/app-string-literal/src/lib/multi-language-textarea.component.ts b/libs/vre/shared/app-string-literal/src/lib/multi-language-textarea.component.ts index 53a310ece9..a2138342fe 100644 --- a/libs/vre/shared/app-string-literal/src/lib/multi-language-textarea.component.ts +++ b/libs/vre/shared/app-string-literal/src/lib/multi-language-textarea.component.ts @@ -7,7 +7,6 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { NgxsStoreModule } from '@dasch-swiss/vre/shared/app-state'; -import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { HumanReadableErrorPipe } from './human-readable-error.pipe'; import { MultiLanguageFormService } from './multi-language-form.service'; @@ -25,7 +24,6 @@ import { MultiLanguageFormService } from './multi-language-form.service'; FormsModule, ReactiveFormsModule, NgxsStoreModule, - NgxsStoragePluginModule, HumanReadableErrorPipe, ], template: ` diff --git a/package-lock.json b/package-lock.json index 1422dcdf87..f17a9922eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dsp-app", - "version": "11.1.7", + "version": "11.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsp-app", - "version": "11.1.7", + "version": "11.2.0", "dependencies": { "@angular/animations": "^16.2.12", "@angular/cdk": "^16.2.12", @@ -19,7 +19,7 @@ "@angular/platform-browser-dynamic": "^16.2.12", "@angular/router": "^16.2.12", "@ckeditor/ckeditor5-angular": "^5.2.0", - "@dasch-swiss/dsp-js": "^9.1.9", + "@dasch-swiss/dsp-js": "^9.1.10", "@dasch-swiss/jdnconvertiblecalendar": "^0.0.3", "@dasch-swiss/jdnconvertiblecalendardateadapter": "^1.1.1", "@datadog/browser-logs": "^4.42.2", @@ -30,7 +30,6 @@ "@ngxs/devtools-plugin": "^3.8.1", "@ngxs/logger-plugin": "^3.8.1", "@ngxs/router-plugin": "^3.8.1", - "@ngxs/storage-plugin": "^3.8.1", "@ngxs/store": "^3.8.1", "@nx/angular": "16.5.3", "angular-split": "^14.0.0", @@ -4713,9 +4712,9 @@ } }, "node_modules/@dasch-swiss/dsp-js": { - "version": "9.1.9", - "resolved": "https://npm.pkg.github.com/download/@dasch-swiss/dsp-js/9.1.9/9e8796e0a2836099b8e17ad3dbadd24c758b7a70", - "integrity": "sha512-DPOQQ5VpElfdlLXG6Z3tQErO2awj5HryQUWPYUVcKRSJOnuV0BJS7Dq8k+BpHUFrDdu5UJwwUcaIyH6GsKXMWQ==", + "version": "9.1.10", + "resolved": "https://npm.pkg.github.com/download/@dasch-swiss/dsp-js/9.1.10/cf3cdebda29cdeb7bfaad2477efac21733cdbfbc", + "integrity": "sha512-agtPhTBWE8k6CnOO4zj3vIh1DQhfq5S6dfrcI0/Y2V8QblA2uC6/NEliGzsv2qG/sBvRZuqJt99gX/flgQeVPQ==", "license": "AGPL-3.0", "dependencies": { "@babel/helper-compilation-targets": "^7.16.7", @@ -7210,23 +7209,6 @@ "rxjs": ">=6.5.5" } }, - "node_modules/@ngxs/storage-plugin": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@ngxs/storage-plugin/-/storage-plugin-3.8.2.tgz", - "integrity": "sha512-GCDyGBJtfB3sjj+JIH5Z35TxKsiwJsP+LhHr9Wnf57LBGYNZKR1HXN1CmwHYBpmQzxKkSqCbD03HIk+cTvioLw==", - "dependencies": { - "tslib": "^2.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ngxs" - }, - "peerDependencies": { - "@angular/core": ">=12.0.0 <18.0.0", - "@ngxs/store": "^3.8.2 || ^3.8.2-dev", - "rxjs": ">=6.5.5" - } - }, "node_modules/@ngxs/store": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@ngxs/store/-/store-3.8.2.tgz", diff --git a/package.json b/package.json index a037ea6645..f873d266fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsp-app", - "version": "11.1.7", + "version": "11.2.0", "repository": { "type": "git", "url": "https://github.com/dasch-swiss/dsp-app.git" @@ -41,7 +41,7 @@ "@angular/platform-browser-dynamic": "^16.2.12", "@angular/router": "^16.2.12", "@ckeditor/ckeditor5-angular": "^5.2.0", - "@dasch-swiss/dsp-js": "^9.1.9", + "@dasch-swiss/dsp-js": "^9.1.10", "@dasch-swiss/jdnconvertiblecalendar": "^0.0.3", "@dasch-swiss/jdnconvertiblecalendardateadapter": "^1.1.1", "@datadog/browser-logs": "^4.42.2", @@ -52,7 +52,6 @@ "@ngxs/devtools-plugin": "^3.8.1", "@ngxs/logger-plugin": "^3.8.1", "@ngxs/router-plugin": "^3.8.1", - "@ngxs/storage-plugin": "^3.8.1", "@ngxs/store": "^3.8.1", "@nx/angular": "16.5.3", "angular-split": "^14.0.0",