From da5f2894d24be67ec5106696ac11f2848062b0f5 Mon Sep 17 00:00:00 2001 From: Julien Schneider Date: Mon, 18 Dec 2023 14:47:36 +0100 Subject: [PATCH] feat: create api routes for /admin (#1318) --- .github/workflows/main.yml | 2 +- apps/dateAdapter/src/app/app.component.ts | 2 +- apps/dsp-app/src/app/app-routing.module.ts | 3 +- apps/dsp-app/src/app/app.module.ts | 36 +- .../confirmation-dialog.component.ts | 1 - .../app/main/guard/auth-guard.component.ts | 14 + apps/dsp-app/src/app/main/guard/auth.guard.ts | 71 +- .../http-interceptors/auth-interceptor.ts | 33 + .../data-models/data-models.component.ts | 5 +- .../list-info-form.component.ts | 401 +++-- .../edit-list-item.component.ts | 393 +++-- .../list-item-form.component.ts | 656 ++++---- .../list/list-item/list-item.component.ts | 343 ++--- .../src/app/project/list/list.component.ts | 83 +- apps/dsp-app/src/app/project/project-base.ts | 16 +- .../project-form/project-form.component.ts | 878 ++++++----- .../projects-list/projects-list.component.ts | 362 +++-- .../users/users-list/users-list.component.ts | 801 +++++----- .../src/app/user/account/account.component.ts | 164 +- .../password-form/password-form.component.ts | 6 +- .../app/user/user-form/user-form.component.ts | 743 +++++---- .../create-link-resource.component.ts | 365 +++-- .../properties/properties.component.ts | 1369 ++++++++--------- .../representation/text/text.component.ts | 2 +- .../color-picker/color-picker.component.ts | 3 +- .../date-value-handler.component.ts | 8 +- .../interval-input.component.ts | 2 +- .../values/list-value/list-value.component.ts | 335 ++-- .../time-input/time-input.component.ts | 2 +- .../fulltext-search.component.spec.ts | 4 - .../fulltext-search.component.ts | 840 +++++----- .../advanced-search.service.ts | 58 +- .../advanced-search-store.service.ts | 8 +- libs/vre/shared/app-api/.eslintrc.json | 3 + libs/vre/shared/app-api/README.md | 7 + libs/vre/shared/app-api/jest.config.ts | 22 + libs/vre/shared/app-api/project.json | 34 + libs/vre/shared/app-api/src/index.ts | 12 + .../src/lib/interfaces/graph.interface.ts | 3 + .../administrative-permission-api.service.ts | 38 + .../lib/services/admin/group-api.service.ts | 41 + .../lib/services/admin/list-api.service.ts | 168 ++ .../services/admin/permission-api.service.ts | 95 ++ .../lib/services/admin/project-api.service.ts | 61 + .../lib/services/admin/user-api.service.ts | 91 ++ .../app-api/src/lib/services/base-api.ts | 7 + .../src/lib/services/health-api.service.ts | 17 + .../services/v2/authentication-api.service.ts | 30 + .../services/v2/list/list-node.interface.ts | 6 + .../services/v2/list/list-v2-api.service.ts | 23 + .../v2/ontology/can-do-response.interface.ts | 4 + .../v2/ontology/create-ontology.interface.ts | 6 + ...create-resource-class-payload.interface.ts | 5 + .../create-resource-class.interface.ts | 7 + .../create-resource-property.interface.ts | 12 + .../v2/ontology/i-has-property.interface.ts | 28 + .../ontology/ontology-metadata.interface.ts | 8 + .../v2/ontology/ontology-v2-api.service.ts | 140 ++ .../v2/ontology/read-ontology.interface.ts | 6 + ...definition-with-all-languages.interface.ts | 6 + ...definition-with-all-languages.interface.ts | 13 + .../services/v2/ontology/string-literal.v2.ts | 4 + .../update-resource-class.interface.ts | 7 + ...resource-property-gui-element.interface.ts | 4 + ...te-resource-property-response.interface.ts | 6 + .../update-resource-property.interface.ts | 7 + .../src/lib/services/version-api.service.ts | 17 + libs/vre/shared/app-api/src/test-setup.ts | 8 + libs/vre/shared/app-api/tsconfig.json | 29 + libs/vre/shared/app-api/tsconfig.lib.json | 17 + libs/vre/shared/app-api/tsconfig.spec.json | 16 + .../src/lib/app-error-handler.ts | 5 - .../src/lib/project.service.ts | 2 +- .../shared/app-session/src/lib/app-session.ts | 45 +- .../app-session/src/lib/auth.service.ts | 151 +- .../current-project.selectors.ts | 0 .../current-project.state-model.ts | 0 .../app-state/src/lib/lists/lists.state.ts | 190 +-- .../src/lib/ontologies/ontologies.state.ts | 793 +++++----- .../src/lib/projects/projects.state-model.ts | 16 +- .../src/lib/projects/projects.state.ts | 588 +++---- .../src/lib/user/user.state-model.ts | 18 +- .../app-state/src/lib/user/user.state.ts | 395 +++-- tsconfig.base.json | 9 +- 84 files changed, 6042 insertions(+), 5187 deletions(-) create mode 100644 apps/dsp-app/src/app/main/guard/auth-guard.component.ts create mode 100644 apps/dsp-app/src/app/main/http-interceptors/auth-interceptor.ts create mode 100644 libs/vre/shared/app-api/.eslintrc.json create mode 100644 libs/vre/shared/app-api/README.md create mode 100644 libs/vre/shared/app-api/jest.config.ts create mode 100644 libs/vre/shared/app-api/project.json create mode 100644 libs/vre/shared/app-api/src/index.ts create mode 100644 libs/vre/shared/app-api/src/lib/interfaces/graph.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/admin/administrative-permission-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/admin/group-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/admin/list-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/admin/permission-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/admin/project-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/admin/user-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/base-api.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/health-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/authentication-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/list/list-node.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/list/list-v2-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/can-do-response.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/create-ontology.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class-payload.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-property.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/i-has-property.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-metadata.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-v2-api.service.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/read-ontology.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-class-definition-with-all-languages.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-property-definition-with-all-languages.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/string-literal.v2.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-class.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-gui-element.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-response.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property.interface.ts create mode 100644 libs/vre/shared/app-api/src/lib/services/version-api.service.ts create mode 100644 libs/vre/shared/app-api/src/test-setup.ts create mode 100644 libs/vre/shared/app-api/tsconfig.json create mode 100644 libs/vre/shared/app-api/tsconfig.lib.json create mode 100644 libs/vre/shared/app-api/tsconfig.spec.json create mode 100644 libs/vre/shared/app-state/src/lib/current-project/current-project.selectors.ts create mode 100644 libs/vre/shared/app-state/src/lib/current-project/current-project.state-model.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a43ce3947..ef1321387e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: - name: "Run npm install" run: | npm install - npx nx run-many --all --target=lint + npx nx run-many --all --target=lint --skip-nx-cache # npx nx run-many --all --target=test --configuration=ci env: TZ: Europe/Zurich diff --git a/apps/dateAdapter/src/app/app.component.ts b/apps/dateAdapter/src/app/app.component.ts index 493ba243c6..49d9c1d9a8 100644 --- a/apps/dateAdapter/src/app/app.component.ts +++ b/apps/dateAdapter/src/app/app.component.ts @@ -111,7 +111,7 @@ export class AppComponent { `, styleUrls: [], }) -export class HeaderComponent implements OnInit { +export class HeaderComponent implements OnInit { constructor( private _calendar: MatCalendar, private _dateAdapter: DateAdapter, diff --git a/apps/dsp-app/src/app/app-routing.module.ts b/apps/dsp-app/src/app/app-routing.module.ts index acd89038b2..861b61bc45 100644 --- a/apps/dsp-app/src/app/app-routing.module.ts +++ b/apps/dsp-app/src/app/app-routing.module.ts @@ -92,7 +92,8 @@ const routes: Routes = [ }, { path: `${RouteConstants.list}/:${RouteConstants.listParameter}`, - component: ListComponent + component: ListComponent, + canActivate: [AuthGuard] }, { path: RouteConstants.settings, diff --git a/apps/dsp-app/src/app/app.module.ts b/apps/dsp-app/src/app/app.module.ts index bf79b0f2e5..91837142e9 100644 --- a/apps/dsp-app/src/app/app.module.ts +++ b/apps/dsp-app/src/app/app.module.ts @@ -1,7 +1,11 @@ /* eslint-disable max-len */ import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; -import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { + HTTP_INTERCEPTORS, + HttpClient, + HttpClientModule, +} from '@angular/common/http'; import { ErrorHandler, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; @@ -13,7 +17,14 @@ import { AngularSplitModule } from 'angular-split'; import { MatJDNConvertibleCalendarDateAdapterModule } from '@dasch-swiss/jdnconvertiblecalendardateadapter'; import { PdfViewerModule } from 'ng2-pdf-viewer'; import { ColorPickerModule } from 'ngx-color-picker'; -import { AppConfigService } from '@dasch-swiss/vre/shared/app-config'; +import { + AppConfigService, + buildTagFactory, + BuildTagToken, + DspApiConfigToken, + DspAppConfigToken, + DspInstrumentationToken, +} from '@dasch-swiss/vre/shared/app-config'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { ConfirmationDialogComponent } from './main/action/confirmation-dialog/confirmation-dialog.component'; @@ -22,11 +33,6 @@ import { LoginFormComponent } from './main/action/login-form/login-form.componen import { SelectedResourcesComponent } from './main/action/selected-resources/selected-resources.component'; import { SortButtonComponent } from './main/action/sort-button/sort-button.component'; import { CookiePolicyComponent } from './main/cookie-policy/cookie-policy.component'; -import { - DspApiConfigToken, - DspAppConfigToken, - DspInstrumentationToken, -} from '@dasch-swiss/vre/shared/app-config'; import { DialogHeaderComponent } from './main/dialog/dialog-header/dialog-header.component'; import { DialogComponent } from './main/dialog/dialog.component'; import { AdminImageDirective } from './main/directive/admin-image/admin-image.directive'; @@ -150,19 +156,17 @@ import { CommentFormComponent } from './workspace/resource/values/comment-form/c import { DataModelsComponent } from './project/data-models/data-models.component'; import { ResourceClassPropertyInfoComponent } from '@dsp-app/src/app/project/ontology/resource-class-info/resource-class-property-info/resource-class-property-info.component'; import { AppLoggingService } from '@dasch-swiss/vre/shared/app-logging'; -import { - buildTagFactory, - BuildTagToken, -} from '@dasch-swiss/vre/shared/app-config'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { AppDatePickerComponent } from '@dasch-swiss/vre/shared/app-date-picker'; import { AdvancedSearchComponent } from '@dasch-swiss/vre/advanced-search'; import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { apiConnectionTokenProvider } from './providers/api-connection-token.provider'; import { NgxsStoreModule } from '@dasch-swiss/vre/shared/app-state'; -import { AppProgressIndicatorComponent } from "@dasch-swiss/vre/shared/app-progress-indicator"; -import {AppStringLiteralComponent} from "@dasch-swiss/vre/shared/app-string-literal"; +import { AppProgressIndicatorComponent } from '@dasch-swiss/vre/shared/app-progress-indicator'; +import { AppStringLiteralComponent } from '@dasch-swiss/vre/shared/app-string-literal'; import { IsFalsyPipe } from './main/pipes/isFalsy.piipe'; +import { AuthGuardComponent } from '@dsp-app/src/app/main/guard/auth-guard.component'; +import { AuthInterceptor } from './main/http-interceptors/auth-interceptor'; // translate: AoT requires an exported function for factories export function httpLoaderFactory(httpClient: HttpClient) { @@ -180,6 +184,7 @@ export function httpLoaderFactory(httpClient: HttpClient) { AppComponent, ArchiveComponent, AudioComponent, + AuthGuardComponent, AvTimelineComponent, DescriptionComponent, BooleanValueComponent, @@ -360,6 +365,11 @@ export function httpLoaderFactory(httpClient: HttpClient) { useClass: AppErrorHandler, deps: [AppLoggingService], }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + }, ], bootstrap: [AppComponent], }) diff --git a/apps/dsp-app/src/app/main/action/confirmation-dialog/confirmation-dialog.component.ts b/apps/dsp-app/src/app/main/action/confirmation-dialog/confirmation-dialog.component.ts index 8c7213b9c4..6c0d540024 100644 --- a/apps/dsp-app/src/app/main/action/confirmation-dialog/confirmation-dialog.component.ts +++ b/apps/dsp-app/src/app/main/action/confirmation-dialog/confirmation-dialog.component.ts @@ -31,7 +31,6 @@ export class ConfirmationDialogComponent { } onConfirmClick(): void { - const z = 0; const payload = new ConfirmationDialogValueDeletionPayload(); payload.confirmed = true; payload.deletionComment = this.confirmationMessageComponent.comment diff --git a/apps/dsp-app/src/app/main/guard/auth-guard.component.ts b/apps/dsp-app/src/app/main/guard/auth-guard.component.ts new file mode 100644 index 0000000000..7232d78d47 --- /dev/null +++ b/apps/dsp-app/src/app/main/guard/auth-guard.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; + +// empty component used as a redirect when the user logs in +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: '' +}) +export class AuthGuardComponent { + constructor(private router: Router) { + this.router.navigate([RouteConstants.home], { replaceUrl: true }); + } +} 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 4974deea32..1273adf844 100644 --- a/apps/dsp-app/src/app/main/guard/auth.guard.ts +++ b/apps/dsp-app/src/app/main/guard/auth.guard.ts @@ -1,22 +1,21 @@ -import { ChangeDetectionStrategy, Component, Inject, Injectable } from '@angular/core'; -import { CanActivate, Router } from '@angular/router'; +import { Inject, Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; import { DOCUMENT } from '@angular/common'; import { Actions, ofActionCompleted, Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { concatMap, map, switchMap } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { ReadUser } from '@dasch-swiss/dsp-js'; import { AuthService } from '@dasch-swiss/vre/shared/app-session'; -import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; -import { CurrentPageSelectors, SetUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; - +import { + CurrentPageSelectors, + SetUserAction, + UserSelectors, +} from '@dasch-swiss/vre/shared/app-state'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AuthGuard implements CanActivate { - - isLoggedIn$: Observable = this._authService.isLoggedIn$; - @Select(UserSelectors.user) user$: Observable; constructor( @@ -24,52 +23,36 @@ export class AuthGuard implements CanActivate { private _authService: AuthService, private actions$: Actions, @Inject(DOCUMENT) private document: Document - ) { - } + ) {} canActivate(): Observable { return this.user$.pipe( switchMap((user) => { - if (!user) { - if (this.store.selectSnapshot(UserSelectors.isLoading)) { - return this.actions$.pipe( - ofActionCompleted(SetUserAction), - concatMap(() => { - return this.isLoggedIn$; - }) - ); - } else { - return this.store.dispatch(new SetUserAction(user)).pipe( - concatMap(() => { - return this.isLoggedIn$; - }) - ); - } + 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)); } - return this.isLoggedIn$; }), + switchMap(() => this._authService.isLoggedIn$), map((isLoggedIn) => { if (isLoggedIn) { return true; + } else { + this._goToHomePage(); + return false; } - this.document.defaultView.location.href = - `${this.document.defaultView.location.href}?` + - `returnLink=${this.store.selectSnapshot( - CurrentPageSelectors.loginReturnLink - )}`; - return false; }) ); } -} -// empty component used as a redirect when the user logs in -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - template: '' -}) -export class AuthGuardComponent { - constructor(private router: Router) { - this.router.navigate([RouteConstants.home], { replaceUrl: true }); + private _goToHomePage() { + this.document.defaultView.location.href = + `${this.document.defaultView.location.href}?` + + `returnLink=${this.store.selectSnapshot( + CurrentPageSelectors.loginReturnLink + )}`; } } 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 new file mode 100644 index 0000000000..d81ee218a0 --- /dev/null +++ b/apps/dsp-app/src/app/main/http-interceptors/auth-interceptor.ts @@ -0,0 +1,33 @@ +import { + HttpHandler, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { AuthService } from '@dasch-swiss/vre/shared/app-session'; +import { AppConfigService } from '@dasch-swiss/vre/shared/app-config'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor( + private _authService: AuthService, + private _appConfigService: AppConfigService + ) {} + + intercept(req: HttpRequest, next: HttpHandler) { + if (!req.url.startsWith(this._appConfigService.dspApiConfig.apiUrl)) { + return next.handle(req); + } + + const authToken = this._authService.getAccessToken(); + if (!authToken) return next.handle(req); + + const authReq = req.clone({ + headers: req.headers.set( + 'Authorization', + `Bearer ${this._authService.getAccessToken()}` + ), + }); + return next.handle(authReq); + } +} diff --git a/apps/dsp-app/src/app/project/data-models/data-models.component.ts b/apps/dsp-app/src/app/project/data-models/data-models.component.ts index 8b433e4a85..4583be34dc 100644 --- a/apps/dsp-app/src/app/project/data-models/data-models.component.ts +++ b/apps/dsp-app/src/app/project/data-models/data-models.component.ts @@ -34,7 +34,7 @@ export class DataModelsComponent extends ProjectBase implements OnInit { if (!uuid) { return of({} as OntologyMetadata[]); } - + return this._store.select(OntologiesSelectors.projectOntologies) .pipe( map(ontologies => { @@ -66,9 +66,6 @@ export class DataModelsComponent extends ProjectBase implements OnInit { ngOnInit(): void { super.ngOnInit(); - const uuid = this._route.parent.snapshot.params.uuid; - //TODO Soft or Hard loading? - //this._store.dispatch(new LoadListsInProjectAction(uuid)); } trackByFn = (index: number, item: ListNodeInfo) => `${index}-${item.id}`; diff --git a/apps/dsp-app/src/app/project/list/list-info-form/list-info-form.component.ts b/apps/dsp-app/src/app/project/list/list-info-form/list-info-form.component.ts index 01fcd7fabe..64032ce59a 100644 --- a/apps/dsp-app/src/app/project/list/list-info-form/list-info-form.component.ts +++ b/apps/dsp-app/src/app/project/list/list-info-form/list-info-form.component.ts @@ -3,247 +3,206 @@ import { ChangeDetectorRef, Component, EventEmitter, - Inject, Input, OnInit, - Output, + Output } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { - ApiResponseData, - ApiResponseError, - CreateListRequest, - KnoraApiConnection, - List, - ListInfoResponse, - ListNodeInfo, - ListResponse, - ProjectResponse, - StringLiteral, - UpdateListInfoRequest, -} 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 { CreateListRequest, List, ListNodeInfo, StringLiteral, UpdateListInfoRequest } from '@dasch-swiss/dsp-js'; +import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { LoadListsInProjectAction } from '@dasch-swiss/vre/shared/app-state'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { Store } from '@ngxs/store'; +import { ListApiService, ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-list-info-form', - templateUrl: './list-info-form.component.html', - styleUrls: ['./list-info-form.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-list-info-form', + templateUrl: './list-info-form.component.html', + styleUrls: ['./list-info-form.component.scss'] }) export class ListInfoFormComponent implements OnInit { - @Input() iri?: string; - - @Input() mode: 'create' | 'update'; - - // project uuid - @Input() projectUuid: string; - - @Input() projectIri: string; - - @Output() closeDialog: EventEmitter = - new EventEmitter(); - - loading: boolean; - - list: ListNodeInfo; - - labels: StringLiteral[]; - comments: StringLiteral[]; - - // possible errors for the label - labelErrors = { - label: { - required: 'A label is required.', - }, - comment: { - required: 'A description is required.', - }, - }; - - saveButtonDisabled = true; - - labelInvalidMessage: string; - commentInvalidMessage: string; - - isLabelTouched = false; - isCommentTouched = false; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _route: ActivatedRoute, - private _router: Router, - private _projectService: ProjectService, - private _cd: ChangeDetectorRef, - private _store: Store, - ) { - // in case of creating new - if (this._route.parent) { - this.mode = 'create'; - // get the uuid of the current project - this._route.parent.paramMap.subscribe((params: Params) => { - this.projectUuid = params.get('uuid'); - - this._dspApiConnection.admin.projectsEndpoint - .getProjectByIri( - this._projectService.uuidToIri(this.projectUuid) - ) - .subscribe( - (response: ApiResponseData) => { - this.projectIri = response.body.project.id; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - }); - } - // in case of edit - if (this._route.firstChild) { - this.mode = 'update'; - // get the uuid of the current project - this._route.firstChild.paramMap.subscribe((params: Params) => { - this.projectUuid = params.get('uuid'); - - this._dspApiConnection.admin.projectsEndpoint - .getProjectByIri( - this._projectService.uuidToIri(this.projectUuid) - ) - .subscribe( - (response: ApiResponseData) => { - this.projectIri = response.body.project.id; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - }); - } - } + @Input() iri?: string; - ngOnInit() { - this.loading = true; - // get list info in case of edit mode - if (this.mode === 'update') { - // edit mode, get list - this._dspApiConnection.admin.listsEndpoint - .getListInfo(this.iri) - .subscribe( - (response: ApiResponseData) => { - this.list = response.body.listinfo; - this.buildLists(response.body.listinfo); - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } else { - // build the form - this.buildLists(); - } - } + @Input() mode: 'create' | 'update'; - buildLists(list?: ListNodeInfo): void { - this.loading = true; - this.labels = []; - this.comments = []; + // project uuid + @Input() projectUuid: string; - if (list && list.id) { - this.labels = list.labels; - this.comments = list.comments; - } + @Input() projectIri: string; - this.loading = false; - } + @Output() closeDialog: EventEmitter = + new EventEmitter(); - submitData(): void { - this.loading = true; - - if (this.mode === 'update') { - // edit mode: update list info - const listInfoUpdateData: UpdateListInfoRequest = - new UpdateListInfoRequest(); - listInfoUpdateData.projectIri = this.projectIri; - listInfoUpdateData.listIri = this.iri; - listInfoUpdateData.labels = this.labels; - listInfoUpdateData.comments = this.comments; - - this._dspApiConnection.admin.listsEndpoint - .updateListInfo(listInfoUpdateData) - .subscribe( - (response: ApiResponseData) => { - this._store.dispatch(new LoadListsInProjectAction(this.projectIri)); - this.loading = false; - this.closeDialog.emit(response.body.listinfo); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - this.loading = false; - } - ); - } else { - // new: create list - const listInfoData: CreateListRequest = new CreateListRequest(); - listInfoData.projectIri = this.projectIri; - listInfoData.labels = this.labels; - listInfoData.comments = this.comments; - - this._dspApiConnection.admin.listsEndpoint - .createList(listInfoData) - .subscribe( - (response: ApiResponseData) => { - this._store.dispatch(new LoadListsInProjectAction(this.projectIri)); - this.loading = false; - // go to the new list page - const array = response.body.list.listinfo.id.split('/'); - const name = array[array.length - 1]; - this._router.navigate([RouteConstants.list, name], { - relativeTo: this._route.parent, - }); - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - this.loading = false; - } - ); - } - } + loading: boolean; + + list: ListNodeInfo; + + labels: StringLiteral[]; + comments: StringLiteral[]; - /** - * reset the form - */ - resetLists(ev: Event, list?: ListNodeInfo) { - ev.preventDefault(); + // possible errors for the label + labelErrors = { + label: { + required: 'A label is required.' + }, + comment: { + required: 'A description is required.' + } + }; + + saveButtonDisabled = true; + + labelInvalidMessage: string; + commentInvalidMessage: string; + + isLabelTouched = false; + isCommentTouched = false; + + constructor( + private _projectApiService: ProjectApiService, + private _listApiService: ListApiService, + private _route: ActivatedRoute, + private _router: Router, + private _projectService: ProjectService, + private _cd: ChangeDetectorRef, + private _store: Store + ) { + // in case of creating new + if (this._route.parent) { + this.mode = 'create'; + // get the uuid of the current project + this._route.parent.paramMap.subscribe((params: Params) => { + this.projectUuid = params.get('uuid'); + + this._projectApiService.get( + this._projectService.uuidToIri(this.projectUuid) + ) + .subscribe( + (response) => { + this.projectIri = response.project.id; + }); + }); + } + // in case of edit + if (this._route.firstChild) { + this.mode = 'update'; + // get the uuid of the current project + this._route.firstChild.paramMap.subscribe((params: Params) => { + this.projectUuid = params.get('uuid'); + + this._projectApiService.get( + this._projectService.uuidToIri(this.projectUuid) + ) + .subscribe( + (response) => { + this.projectIri = response.project.id; + }); + }); + } + } + + ngOnInit() { + this.loading = true; + // get list info in case of edit mode + if (this.mode === 'update') { + // edit mode, get list + this._listApiService.getInfo(this.iri) + .subscribe( + (response) => { + this.list = response.listinfo; + this.buildLists(response.listinfo); + this._cd.markForCheck(); + }); + } else { + // build the form + this.buildLists(); + } + } - list = list ? list : new ListNodeInfo(); + buildLists(list?: ListNodeInfo): void { + this.loading = true; + this.labels = []; + this.comments = []; - this.buildLists(list); + if (list && list.id) { + this.labels = list.labels; + this.comments = list.comments; } - handleData(data: StringLiteral[], type: string) { - switch (type) { - case 'labels': - this.labels = data; - this.labelInvalidMessage = data.length - ? null - : this.labelErrors.label.required; - break; - - case 'comments': - this.comments = data; - this.commentInvalidMessage = data.length - ? null - : this.labelErrors.comment.required; - break; - } - - this.saveButtonDisabled = !this.labels.length || !this.comments.length; + this.loading = false; + } + + submitData(): void { + this.loading = true; + + if (this.mode === 'update') { + // edit mode: update list info + const listInfoUpdateData: UpdateListInfoRequest = + new UpdateListInfoRequest(); + listInfoUpdateData.projectIri = this.projectIri; + listInfoUpdateData.listIri = this.iri; + listInfoUpdateData.labels = this.labels; + listInfoUpdateData.comments = this.comments; + + this._listApiService.updateInfo(listInfoUpdateData.listIri, listInfoUpdateData) + .subscribe( + (response) => { + this._store.dispatch(new LoadListsInProjectAction(this.projectIri)); + this.loading = false; + this.closeDialog.emit(response.listinfo); + }); + } else { + // new: create list + const listInfoData: CreateListRequest = new CreateListRequest(); + listInfoData.projectIri = this.projectIri; + listInfoData.labels = this.labels; + listInfoData.comments = this.comments; + + this._listApiService.create(listInfoData) + .subscribe( + (response) => { + this._store.dispatch(new LoadListsInProjectAction(this.projectIri)); + this.loading = false; + // go to the new list page + const array = response.list.listinfo.id.split('/'); + const name = array[array.length - 1]; + this._router.navigate([RouteConstants.list, name], { + relativeTo: this._route.parent + }); + this._cd.markForCheck(); + }); } + } + + /** + * reset the form + */ + resetLists(ev: Event, list?: ListNodeInfo) { + ev.preventDefault(); + + list = list ? list : new ListNodeInfo(); + + this.buildLists(list); + } + + handleData(data: StringLiteral[], type: string) { + switch (type) { + case 'labels': + this.labels = data; + this.labelInvalidMessage = data.length + ? null + : this.labelErrors.label.required; + break; + + case 'comments': + this.comments = data; + this.commentInvalidMessage = data.length + ? null + : this.labelErrors.comment.required; + break; + } + + this.saveButtonDisabled = !this.labels.length || !this.comments.length; + } } diff --git a/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item.component.ts b/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item.component.ts index bed13a3e48..c7a3c82ba0 100644 --- a/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item.component.ts +++ b/apps/dsp-app/src/app/project/list/list-item-form/edit-list-item/edit-list-item.component.ts @@ -1,230 +1,221 @@ import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Inject, - Input, - OnInit, - Output, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnInit, + Output } from '@angular/core'; import { - ApiResponseData, - ApiResponseError, - ChildNodeInfoResponse, - CreateChildNodeRequest, - KnoraApiConnection, - List, - ListNodeInfo, - ListNodeInfoResponse, - StringLiteral, - UpdateChildNodeRequest, + ApiResponseError, + CreateChildNodeRequest, + KnoraApiConnection, + List, + ListNodeInfo, + StringLiteral, + UpdateChildNodeRequest } 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 { ListApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-edit-list-item', - templateUrl: './edit-list-item.component.html', - styleUrls: ['./edit-list-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-edit-list-item', + templateUrl: './edit-list-item.component.html', + styleUrls: ['./edit-list-item.component.scss'] }) export class EditListItemComponent implements OnInit { - @Input() iri: string; + @Input() iri: string; - @Input() mode: 'insert' | 'update'; + @Input() mode: 'insert' | 'update'; - @Input() parentIri?: string; + @Input() parentIri?: string; - @Input() position?: number; + @Input() position?: number; - @Input() projectIri: string; + @Input() projectIri: string; - @Output() closeDialog: EventEmitter = - new EventEmitter(); + @Output() closeDialog: EventEmitter = + new EventEmitter(); - loading: boolean; + loading: boolean; - // the list node being edited - listNode: ListNodeInfo; + // the list node being edited + listNode: ListNodeInfo; - // local arrays to use when updating the list node - labels: StringLiteral[]; - comments: StringLiteral[]; + // local arrays to use when updating the list node + labels: StringLiteral[]; + comments: StringLiteral[]; - // used to check if request to delete comments should be sent - initialCommentsLength: number; + // used to check if request to delete comments should be sent + initialCommentsLength: number; - /** - * error checking on the following fields - */ - formErrors = { - label: { - required: 'A label is required.', - }, - }; - - /** - * in case of an API error - */ - errorMessage: any; - - saveButtonDisabled = false; - - formInvalidMessage: string; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _projectService: ProjectService, - private _cd: ChangeDetectorRef - ) {} - - ngOnInit(): void { - this.loading = true; - - // if updating a node, get the existing node info - if (this.mode === 'update') { - this._dspApiConnection.admin.listsEndpoint - .getListNodeInfo(this.iri) - .subscribe( - (response: ApiResponseData) => { - this.loading = false; - this.listNode = response.body.nodeinfo; - this.buildForm(response.body.nodeinfo); - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } else { - this.labels = []; - this.comments = []; - - this.loading = false; - } + /** + * error checking on the following fields + */ + formErrors = { + label: { + required: 'A label is required.' } - - /** - * separates the labels and comments of a list node into two local arrays. - * - * @param listNode info about a list node - */ - buildForm(listNode: ListNodeInfo): void { - this.labels = []; - this.comments = []; - - if (listNode && listNode.id) { - this.labels = listNode.labels; - this.comments = listNode.comments; - this.initialCommentsLength = this.comments.length; - } - - this.loading = false; + }; + + /** + * in case of an API error + */ + errorMessage: any; + + saveButtonDisabled = false; + + formInvalidMessage: string; + + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _listApiService: ListApiService, + private _errorHandler: AppErrorHandler, + private _projectService: ProjectService, + private _cd: ChangeDetectorRef + ) { + } + + ngOnInit(): void { + this.loading = true; + + // if updating a node, get the existing node info + if (this.mode === 'update') { + this._listApiService + .getNodeInfo(this.iri) + .subscribe( + response => { + this.loading = false; + this.listNode = response.nodeinfo; + this.buildForm(response.nodeinfo); + this._cd.markForCheck(); + }); + } else { + this.labels = []; + this.comments = []; + + this.loading = false; } - - /** - * called from the template any time the labels or comments are changed to update the local arrays. - * At least one label is required. Otherwise, the 'update' button will be disabled. - * - * @param data the data that was changed - * @param type the type of data that was changed - */ - handleData(data: StringLiteral[], type: string) { - switch (type) { - case 'labels': - this.labels = data; - break; - - case 'comments': - this.comments = data; - break; - } - - if (this.labels.length === 0) { - // invalid form, don't let user submit - this.saveButtonDisabled = true; - this.formInvalidMessage = this.formErrors.label.required; - } else { - this.saveButtonDisabled = false; - this.formInvalidMessage = null; - } + } + + /** + * separates the labels and comments of a list node into two local arrays. + * + * @param listNode info about a list node + */ + buildForm(listNode: ListNodeInfo): void { + this.labels = []; + this.comments = []; + + if (listNode && listNode.id) { + this.labels = listNode.labels; + this.comments = listNode.comments; + this.initialCommentsLength = this.comments.length; } - /** - * called from the template when the 'submit' button is clicked in update mode. - * sends a request to DSP-API to update the list child node with the data inside the two local arrays. - */ - updateChildNode() { - const childNodeUpdateData: UpdateChildNodeRequest = - new UpdateChildNodeRequest(); - childNodeUpdateData.projectIri = this.projectIri; - childNodeUpdateData.listIri = this.iri; - childNodeUpdateData.labels = this.labels; - childNodeUpdateData.comments = - this.comments.length > 0 ? this.comments : undefined; - - this._dspApiConnection.admin.listsEndpoint - .updateChildNode(childNodeUpdateData) - .subscribe( - (response: ApiResponseData) => { - // if initialCommentsLength is not equal to 0 and the comment is now empty, send request to delete comment - if ( - this.initialCommentsLength !== 0 && - !childNodeUpdateData.comments - ) { - this._dspApiConnection.admin.listsEndpoint - .deleteChildComments(childNodeUpdateData.listIri) - .subscribe( - // eslint-disable-next-line @typescript-eslint/no-empty-function - () => {}, - (error: ApiResponseError) => - this._errorHandler.showMessage(error) - ); - } - this.loading = false; - this.closeDialog.emit(response.body.nodeinfo); - }, - (error: ApiResponseError) => { - this.errorMessage = error; - this.loading = false; - } - ); + this.loading = false; + } + + /** + * called from the template any time the labels or comments are changed to update the local arrays. + * At least one label is required. Otherwise, the 'update' button will be disabled. + * + * @param data the data that was changed + * @param type the type of data that was changed + */ + handleData(data: StringLiteral[], type: string) { + switch (type) { + case 'labels': + this.labels = data; + break; + + case 'comments': + this.comments = data; + break; } - /** - * called from the template when the 'submit' button is clicked in insert mode. - * Sends a request to DSP-API to insert a new list child node in the provided position. - */ - insertChildNode() { - const createChildNodeRequest: CreateChildNodeRequest = - new CreateChildNodeRequest(); - createChildNodeRequest.name = - this._projectService.iriToUuid(this.projectIri) + - '-' + - Math.random().toString(36).substring(2) + - Math.random().toString(36).substring(2); - createChildNodeRequest.parentNodeIri = this.parentIri; - createChildNodeRequest.labels = this.labels; - createChildNodeRequest.comments = - this.comments.length > 0 ? this.comments : undefined; - createChildNodeRequest.projectIri = this.projectIri; - createChildNodeRequest.position = this.position; - - this._dspApiConnection.admin.listsEndpoint - .createChildNode(createChildNodeRequest) - .subscribe( - (response: ApiResponseData) => { - this.loading = false; - this.closeDialog.emit(response.body.nodeinfo); - }, - (error: ApiResponseError) => { - this.errorMessage = error; - this.loading = false; - } - ); + if (this.labels.length === 0) { + // invalid form, don't let user submit + this.saveButtonDisabled = true; + this.formInvalidMessage = this.formErrors.label.required; + } else { + this.saveButtonDisabled = false; + this.formInvalidMessage = null; } + } + + /** + * called from the template when the 'submit' button is clicked in update mode. + * sends a request to DSP-API to update the list child node with the data inside the two local arrays. + */ + updateChildNode() { + const childNodeUpdateData: UpdateChildNodeRequest = + new UpdateChildNodeRequest(); + childNodeUpdateData.projectIri = this.projectIri; + childNodeUpdateData.listIri = this.iri; + childNodeUpdateData.labels = this.labels; + childNodeUpdateData.comments = + this.comments.length > 0 ? this.comments : undefined; + + this._listApiService + .updateChildNode(childNodeUpdateData.listIri, childNodeUpdateData) + .subscribe( + response => { + // if initialCommentsLength is not equal to 0 and the comment is now empty, send request to delete comment + if ( + this.initialCommentsLength !== 0 && + !childNodeUpdateData.comments + ) { + this._listApiService + .deleteChildComments(childNodeUpdateData.listIri) + .subscribe(); + } + this.loading = false; + this.closeDialog.emit(response.nodeinfo); + }, + (error: ApiResponseError) => { + this.errorMessage = error; + this.loading = false; + } + ); + } + + /** + * called from the template when the 'submit' button is clicked in insert mode. + * Sends a request to DSP-API to insert a new list child node in the provided position. + */ + insertChildNode() { + const createChildNodeRequest: CreateChildNodeRequest = + new CreateChildNodeRequest(); + createChildNodeRequest.name = + this._projectService.iriToUuid(this.projectIri) + + '-' + + Math.random().toString(36).substring(2) + + Math.random().toString(36).substring(2); + createChildNodeRequest.parentNodeIri = this.parentIri; + createChildNodeRequest.labels = this.labels; + createChildNodeRequest.comments = + this.comments.length > 0 ? this.comments : undefined; + createChildNodeRequest.projectIri = this.projectIri; + createChildNodeRequest.position = this.position; + + this._listApiService + .createChildNode(createChildNodeRequest.parentNodeIri, createChildNodeRequest) + .subscribe( + response => { + this.loading = false; + this.closeDialog.emit(response.nodeinfo); + }, + (error: ApiResponseError) => { + this.errorMessage = error; + this.loading = false; + } + ); + } } diff --git a/apps/dsp-app/src/app/project/list/list-item-form/list-item-form.component.ts b/apps/dsp-app/src/app/project/list/list-item-form/list-item-form.component.ts index 4d64a37bc2..25d8819f23 100644 --- a/apps/dsp-app/src/app/project/list/list-item-form/list-item-form.component.ts +++ b/apps/dsp-app/src/app/project/list/list-item-form/list-item-form.component.ts @@ -1,364 +1,350 @@ +import { animate, state, style, transition, trigger } from '@angular/animations'; import { - animate, - state, - style, - transition, - trigger, -} from '@angular/animations'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Inject, - Input, - OnInit, - Output, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnInit, + Output } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { - ApiResponseData, - ApiResponseError, - ChildNodeInfo, - CreateChildNodeRequest, - DeleteListNodeResponse, - KnoraApiConnection, - ListInfoResponse, - ListNode, - ListNodeInfoResponse, - StringLiteral, + ApiResponseError, + ChildNodeInfo, + CreateChildNodeRequest, + KnoraApiConnection, + ListInfoResponse, + ListNode, + StringLiteral } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { DialogComponent } from '../../../main/dialog/dialog.component'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; +import { ListApiService } from '@dasch-swiss/vre/shared/app-api'; export class ListNodeOperation { - operation: 'create' | 'insert' | 'update' | 'delete' | 'reposition'; - listNode: ListNode; + operation: 'create' | 'insert' | 'update' | 'delete' | 'reposition'; + listNode: ListNode; } @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-list-item-form', - templateUrl: './list-item-form.component.html', - styleUrls: ['./list-item-form.component.scss'], - animations: [ - // the fade-in/fade-out animation. - // https://www.kdechant.com/blog/angular-animations-fade-in-and-fade-out - trigger('simpleFadeAnimation', [ - // the "in" style determines the "resting" state of the element when it is visible. - state('in', style({ opacity: 1 })), - - // fade in when created. - transition(':enter', [ - // the styles start from this point when the element appears - style({ opacity: 0 }), - // and animate toward the "in" state above - animate(150), - ]), - - // fade out when destroyed. - transition( - ':leave', - // fading out uses a different syntax, with the "style" being passed into animate() - animate(150, style({ opacity: 0 })) - ), - ]), - ], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-list-item-form', + templateUrl: './list-item-form.component.html', + styleUrls: ['./list-item-form.component.scss'], + animations: [ + // the fade-in/fade-out animation. + // https://www.kdechant.com/blog/angular-animations-fade-in-and-fade-out + trigger('simpleFadeAnimation', [ + // the "in" style determines the "resting" state of the element when it is visible. + state('in', style({ opacity: 1 })), + + // fade in when created. + transition(':enter', [ + // the styles start from this point when the element appears + style({ opacity: 0 }), + // and animate toward the "in" state above + animate(150) + ]), + + // fade out when destroyed. + transition( + ':leave', + // fading out uses a different syntax, with the "style" being passed into animate() + animate(150, style({ opacity: 0 })) + ) + ]) + ] }) export class ListItemFormComponent implements OnInit { - /** - * node id, in case of edit item - */ - @Input() iri?: string; - - /** - * project uuid - */ - @Input() projectUuid?: string; - - /** - * project status - */ - @Input() projectStatus?: boolean; - - /** - * project id - */ - @Input() projectIri?: string; - - /** - * parent node id - */ - @Input() parentIri?: string; - - @Input() labels?: StringLiteral[]; - - // set main / pre-defined language - @Input() language?: string; - - @Input() position: number; - - // is this node in the last position of the list - @Input() lastPosition = false; - - @Input() newNode = false; - - @Output() refreshParent: EventEmitter = - new EventEmitter(); - - @Input() isAdmin = false; - - loading: boolean; - - initComponent: boolean; - - placeholder = 'Append item to '; - - showActionBubble = false; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _dialog: MatDialog, - private _cd: ChangeDetectorRef, - ) {} - - ngOnInit() { - this.initComponent = true; - - if (this.labels && this.labels.length > 0) { - this.placeholder = 'Edit item '; - } - - // it can be used in the input placeholder - if (this.newNode) { - this._dspApiConnection.admin.listsEndpoint - .getListNodeInfo(this.parentIri) - .subscribe( - ( - response: ApiResponseData< - ListNodeInfoResponse | ListInfoResponse - > - ) => { - if (response.body instanceof ListInfoResponse) { - // root node - this.placeholder += - response.body.listinfo.labels[0].value; - } else { - // child node - this.placeholder += - response.body.nodeinfo.labels[0].value; - } - - this.initComponent = false; - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } - } + /** + * node id, in case of edit item + */ + @Input() iri?: string; - /** - * called from the template when the plus button is clicked. - * Sends the info to make a new child node to DSP-API and refreshes the UI to show the newly added node at the end of the list. - */ - createChildNode() { - if (!this.labels.length) { - return; - } - - this.loading = true; - - // generate the data payload - const childNode: CreateChildNodeRequest = new CreateChildNodeRequest(); - childNode.parentNodeIri = this.parentIri; - childNode.projectIri = this.projectIri; - childNode.name = - this.projectUuid + - '-' + - Math.random().toString(36).substring(2) + - Math.random().toString(36).substring(2); - - // initialize labels - let i = 0; - for (const l of this.labels) { - childNode.labels[i] = new StringLiteral(); - childNode.labels[i].language = l.language; - childNode.labels[i].value = l.value; - i++; - } - // childNode.comments = []; // --> TODO comments are not yet implemented in the template - - // init data to emit to parent - const listNodeOperation: ListNodeOperation = new ListNodeOperation(); - - // send payload to dsp-api's api - this._dspApiConnection.admin.listsEndpoint - .createChildNode(childNode) - .subscribe( - (response: ApiResponseData) => { - // this needs to return a ListNode as opposed to a ListNodeInfo, so we make one - listNodeOperation.listNode = new ListNode(); - listNodeOperation.listNode.hasRootNode = - response.body.nodeinfo.hasRootNode; - listNodeOperation.listNode.id = response.body.nodeinfo.id; - listNodeOperation.listNode.labels = - response.body.nodeinfo.labels; - listNodeOperation.listNode.name = - response.body.nodeinfo.name; - listNodeOperation.listNode.position = - response.body.nodeinfo.position; - listNodeOperation.operation = 'create'; - this.refreshParent.emit(listNodeOperation); - this.loading = false; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } + /** + * project uuid + */ + @Input() projectUuid?: string; - /** - * called from the template any time the label changes. - * Currently only implemented for labels because entering comments is not yet supported. - * - * @param data the data that was changed. - */ - handleData(data: StringLiteral[]) { - // this shouldn't run on the init... - if (!this.initComponent) { - this.labels = data; - } - } + /** + * project status + */ + @Input() projectStatus?: boolean; - /** - * show action bubble with various CRUD buttons when hovered over. - */ - mouseEnter() { - if (this.isAdmin) { - this.showActionBubble = true; - } - } + /** + * project id + */ + @Input() projectIri?: string; - /** - * hide action bubble with various CRUD buttons when not hovered over. - */ - mouseLeave() { - this.showActionBubble = false; - } + /** + * parent node id + */ + @Input() parentIri?: string; - /** - * called when the 'edit' or 'delete' button is clicked. - * - * @param mode mode to tell DialogComponent which part of the template to show. - * @param name label of the node; for now this is always the first label in the array. - * @param iri iri of the node. - */ - openDialog(mode: string, name: string, iri?: string): void { - const dialogConfig: MatDialogConfig = { - width: '640px', - position: { - top: '112px', - }, - data: { - mode: mode, - title: (mode === 'editListNode' || mode === 'deleteListNode') ? name : '', - id: iri, - project: this.projectIri, - projectUuid: this.projectUuid, - parentIri: this.parentIri, - position: this.position, - }, - }; - - // open the dialog box - const dialogRef = this._dialog.open(DialogComponent, dialogConfig); - - dialogRef.afterClosed().subscribe((data: ChildNodeInfo | boolean) => { - // init data to emit to parent - const listNodeOperation = new ListNodeOperation(); - - if (mode === 'insertListNode' && data) { - // the call to DSP-API to insert the new node is done in the child component - listNodeOperation.listNode = data as ListNode; - listNodeOperation.operation = 'insert'; - - this.refreshParent.emit(listNodeOperation); - } else if (mode === 'editListNode' && data) { - // update - // the call to DSP-API to update the node is done in the child component - listNodeOperation.listNode = data as ListNode; - listNodeOperation.operation = 'update'; - - // emit data to parent to update the view - this.refreshParent.emit(listNodeOperation); - this.labels = (data as ChildNodeInfo).labels; - } else if ( - mode === 'deleteListNode' && - typeof data === 'boolean' && - data === true - ) { - // delete - // delete the node - this._dspApiConnection.admin.listsEndpoint - .deleteListNode(iri) - .subscribe( - (response: ApiResponseData) => { - listNodeOperation.listNode = response.body.node; - listNodeOperation.operation = 'delete'; - - // emit data to parent to update the view - this.refreshParent.emit(listNodeOperation); - }, - (error: ApiResponseError) => { - // 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) { - const errorDialogConfig: MatDialogConfig = { - width: '640px', - position: { - top: '112px', - }, - data: { mode: 'deleteListNodeError' }, - }; - - // open the dialog box - this._dialog.open( - DialogComponent, - errorDialogConfig - ); - } else { - // use default error behavior - this._errorHandler.showMessage(error); - } - } - ); - } - }); + @Input() labels?: StringLiteral[]; + + // set main / pre-defined language + @Input() language?: string; + + @Input() position: number; + + // is this node in the last position of the list + @Input() lastPosition = false; + + @Input() newNode = false; + + @Output() refreshParent: EventEmitter = + new EventEmitter(); + + @Input() isAdmin = false; + + loading: boolean; + + initComponent: boolean; + + placeholder = 'Append item to '; + + showActionBubble = false; + + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _listApiService: ListApiService, + private _errorHandler: AppErrorHandler, + private _dialog: MatDialog, + private _cd: ChangeDetectorRef + ) { + } + + ngOnInit() { + this.initComponent = true; + + if (this.labels && this.labels.length > 0) { + this.placeholder = 'Edit item '; } - /** - * called from the template when either of the two reposition buttons is clicked - * @param direction in which direction the node should move - */ - repositionNode(direction: 'up' | 'down') { - const listNodeOperation = new ListNodeOperation(); + // it can be used in the input placeholder + if (this.newNode) { + this._listApiService + .getNodeInfo(this.parentIri) + .subscribe( + response => { + if (response instanceof ListInfoResponse) { + // root node + this.placeholder += + response.listinfo.labels[0].value; + } else { + // child node + this.placeholder += + response.nodeinfo.labels[0].value; + } - listNodeOperation.operation = 'reposition'; - listNodeOperation.listNode = new ListNode(); + this.initComponent = false; + this._cd.markForCheck(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } + } + + /** + * called from the template when the plus button is clicked. + * Sends the info to make a new child node to DSP-API and refreshes the UI to show the newly added node at the end of the list. + */ + createChildNode() { + if (!this.labels.length) { + return; + } - // set desired node position - if (direction === 'up') { - listNodeOperation.listNode.position = this.position - 1; - } else { - listNodeOperation.listNode.position = this.position + 1; - } + this.loading = true; + + // generate the data payload + const childNode: CreateChildNodeRequest = new CreateChildNodeRequest(); + childNode.parentNodeIri = this.parentIri; + childNode.projectIri = this.projectIri; + childNode.name = + this.projectUuid + + '-' + + Math.random().toString(36).substring(2) + + Math.random().toString(36).substring(2); + + // initialize labels + let i = 0; + for (const l of this.labels) { + childNode.labels[i] = new StringLiteral(); + childNode.labels[i].language = l.language; + childNode.labels[i].value = l.value; + i++; + } + // childNode.comments = []; // --> TODO comments are not yet implemented in the template + + // init data to emit to parent + const listNodeOperation: ListNodeOperation = new ListNodeOperation(); + + // send payload to dsp-api's api + this._listApiService + .createChildNode(childNode.parentNodeIri, childNode) + .subscribe( + response => { + // this needs to return a ListNode as opposed to a ListNodeInfo, so we make one + listNodeOperation.listNode = new ListNode(); + listNodeOperation.listNode.hasRootNode = + response.nodeinfo.hasRootNode; + listNodeOperation.listNode.id = response.nodeinfo.id; + listNodeOperation.listNode.labels = + response.nodeinfo.labels; + listNodeOperation.listNode.name = + response.nodeinfo.name; + listNodeOperation.listNode.position = + response.nodeinfo.position; + listNodeOperation.operation = 'create'; + this.refreshParent.emit(listNodeOperation); + this.loading = false; + }); + } + + /** + * called from the template any time the label changes. + * Currently only implemented for labels because entering comments is not yet supported. + * + * @param data the data that was changed. + */ + handleData(data: StringLiteral[]) { + // this shouldn't run on the init... + if (!this.initComponent) { + this.labels = data; + } + } + + /** + * show action bubble with various CRUD buttons when hovered over. + */ + mouseEnter() { + if (this.isAdmin) { + this.showActionBubble = true; + } + } + + /** + * hide action bubble with various CRUD buttons when not hovered over. + */ + mouseLeave() { + this.showActionBubble = false; + } + + /** + * called when the 'edit' or 'delete' button is clicked. + * + * @param mode mode to tell DialogComponent which part of the template to show. + * @param name label of the node; for now this is always the first label in the array. + * @param iri iri of the node. + */ + openDialog(mode: string, name: string, iri?: string): void { + const dialogConfig: MatDialogConfig = { + width: '640px', + position: { + top: '112px' + }, + data: { + mode: mode, + title: (mode === 'editListNode' || mode === 'deleteListNode') ? name : '', + id: iri, + project: this.projectIri, + projectUuid: this.projectUuid, + parentIri: this.parentIri, + position: this.position + } + }; + + // open the dialog box + const dialogRef = this._dialog.open(DialogComponent, dialogConfig); + + dialogRef.afterClosed().subscribe((data: ChildNodeInfo | boolean) => { + // init data to emit to parent + const listNodeOperation = new ListNodeOperation(); + + if (mode === 'insertListNode' && data) { + // the call to DSP-API to insert the new node is done in the child component + listNodeOperation.listNode = data as ListNode; + listNodeOperation.operation = 'insert'; - listNodeOperation.listNode.id = this.iri; + this.refreshParent.emit(listNodeOperation); + } else if (mode === 'editListNode' && data) { + // update + // the call to DSP-API to update the node is done in the child component + listNodeOperation.listNode = data as ListNode; + listNodeOperation.operation = 'update'; + // emit data to parent to update the view this.refreshParent.emit(listNodeOperation); + this.labels = (data as ChildNodeInfo).labels; + } else if ( + mode === 'deleteListNode' && + typeof data === 'boolean' && + data === true + ) { + // delete + // delete the node + this._listApiService + .deleteListNode(iri) + .subscribe( + response => { + listNodeOperation.listNode = response.node; + listNodeOperation.operation = 'delete'; + + // emit data to parent to update the view + this.refreshParent.emit(listNodeOperation); + }, + (error: ApiResponseError) => { + // 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) { + const errorDialogConfig: MatDialogConfig = { + width: '640px', + position: { + top: '112px' + }, + data: { mode: 'deleteListNodeError' } + }; + + // open the dialog box + this._dialog.open( + DialogComponent, + errorDialogConfig + ); + } else { + // use default error behavior + this._errorHandler.showMessage(error); + } + } + ); + } + }); + } + + /** + * called from the template when either of the two reposition buttons is clicked + * @param direction in which direction the node should move + */ + repositionNode(direction: 'up' | 'down') { + const listNodeOperation = new ListNodeOperation(); + + listNodeOperation.operation = 'reposition'; + listNodeOperation.listNode = new ListNode(); + + // set desired node position + if (direction === 'up') { + listNodeOperation.listNode.position = this.position - 1; + } else { + listNodeOperation.listNode.position = this.position + 1; } + + listNodeOperation.listNode.id = this.iri; + + this.refreshParent.emit(listNodeOperation); + } } diff --git a/apps/dsp-app/src/app/project/list/list-item/list-item.component.ts b/apps/dsp-app/src/app/project/list/list-item/list-item.component.ts index 935b94e8dc..0c274c739b 100644 --- a/apps/dsp-app/src/app/project/list/list-item/list-item.component.ts +++ b/apps/dsp-app/src/app/project/list/list-item/list-item.component.ts @@ -1,210 +1,183 @@ import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Inject, - Input, - OnInit, - Output, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output } from '@angular/core'; -import { - ApiResponseData, - ApiResponseError, - KnoraApiConnection, - ListChildNodeResponse, - ListNode, - ListResponse, - RepositionChildNodeRequest, - RepositionChildNodeResponse, -} 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 { ListNode, ListResponse, RepositionChildNodeRequest } from '@dasch-swiss/dsp-js'; import { ListNodeOperation } from '../list-item-form/list-item-form.component'; import { take } from 'rxjs/operators'; +import { ListApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-list-item', - templateUrl: './list-item.component.html', - styleUrls: ['./list-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-list-item', + templateUrl: './list-item.component.html', + styleUrls: ['./list-item.component.scss'] }) export class ListItemComponent implements OnInit { - @Input() list: ListNode[]; + @Input() list: ListNode[]; - @Input() parentIri?: string; + @Input() parentIri?: string; - @Input() projectUuid: string; + @Input() projectUuid: string; - @Input() projectStatus: boolean; + @Input() projectStatus: boolean; - @Input() projectIri: string; + @Input() projectIri: string; - @Input() childNode: boolean; + @Input() childNode: boolean; - @Input() language?: string; + @Input() language?: string; - @Output() refreshChildren: EventEmitter = new EventEmitter< - ListNode[] - >(); + @Output() refreshChildren: EventEmitter = new EventEmitter< + ListNode[] + >(); - // permissions of logged-in user - @Input() isAdmin = false; + // permissions of logged-in user + @Input() isAdmin = false; - expandedNode: string; + expandedNode: string; - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _cd: ChangeDetectorRef, - ) {} + constructor( + private _listApiService: ListApiService, + private _cd: ChangeDetectorRef + ) { + } - ngOnInit() { - // in case of parent node: run the following request to get the entire list - if (!this.childNode) { - this._dspApiConnection.admin.listsEndpoint - .getList(this.parentIri) - .pipe(take(1)) - .subscribe( - (result: ApiResponseData) => { - this.list = result.body.list.children; - this.language = - result.body.list.listinfo.labels[0].language; - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } - } + ngOnInit() { + // in case of parent node: run the following request to get the entire list + if (!this.childNode) { + this._listApiService + .get(this.parentIri) + .pipe(take(1)) + .subscribe( + result => { + if (!(result instanceof ListResponse)) return; - /** - * checks if parent node should show its children. - * @param id id of parent node. - */ - showChildren(id: string): boolean { - return id === this.expandedNode; + this.list = result.list.children; + this.language = + result.list.listinfo.labels[0].language; + this._cd.markForCheck(); + }); } - - /** - * called from template when the 'expand' button is clicked. - * - * @param id id of parent node for which the 'expand' button was clicked. - */ - toggleChildren(id: string) { - if (this.showChildren(id)) { - this.expandedNode = undefined; - } else { - this.expandedNode = id; - } + } + + /** + * checks if parent node should show its children. + * @param id id of parent node. + */ + showChildren(id: string): boolean { + return id === this.expandedNode; + } + + /** + * called from template when the 'expand' button is clicked. + * + * @param id id of parent node for which the 'expand' button was clicked. + */ + toggleChildren(id: string) { + if (this.showChildren(id)) { + this.expandedNode = undefined; + } else { + this.expandedNode = id; } - - /** - * called when the 'refreshParent' event from ListItemFormComponent is triggered. - * - * @param data info about the operation that was performed on the node and should be reflected in the UI. - * @param firstNode states whether the node is a new child node; defaults to false. - */ - updateView(data: ListNodeOperation, firstNode = false) { - // update the view by updating the existing list - if (data instanceof ListNodeOperation) { - switch (data.operation) { - case 'create': { - if (firstNode) { - // in case of new child node, we have to use the children from list - const index: number = this.list.findIndex( - (item) => item.id === this.expandedNode - ); - this.list[index].children.push(data.listNode); - } else { - this.list.push(data.listNode); - } - break; - } - case 'insert': { - // get the corresponding list from the API again and reassign the local list with its response - this._dspApiConnection.admin.listsEndpoint - .getList(this.parentIri) - .subscribe( - ( - result: ApiResponseData< - ListResponse | ListChildNodeResponse - > - ) => { - if (result.body instanceof ListResponse) { - this.list = result.body.list.children; // root node - } else { - this.list = result.body.node.children; // child node - } - - // emit the updated list of children to the parent node - this.refreshChildren.emit(this.list); - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - break; - } - case 'update': { - // use the position from the response from DSP-API to find the correct node to update - this.list[data.listNode.position].labels = - data.listNode.labels; - this.list[data.listNode.position].comments = - data.listNode.comments; - break; - } - case 'delete': { - // conveniently, the response returned by DSP-API contains the entire list without the deleted node within its 'children' - // we can just reassign the list to this - this.list = data.listNode.children; - - // emit the updated list of children to the parent node - this.refreshChildren.emit(this.list); - break; - } - case 'reposition': { - const repositionRequest: RepositionChildNodeRequest = - new RepositionChildNodeRequest(); - repositionRequest.parentNodeIri = this.parentIri; - repositionRequest.position = data.listNode.position; - - // since we don't have any way to know the parent IRI from the ListItemForm component, we need to do the API call here - // --> TODO now we have the parent IRI within the ListItemForm component so we can move this logic there - this._dspApiConnection.admin.listsEndpoint - .repositionChildNode( - data.listNode.id, - repositionRequest - ) - .subscribe( - ( - res: ApiResponseData - ) => { - this.list = res.body.node.children; - - this.refreshChildren.emit(this.list); - this._cd.markForCheck(); - } - ); - break; - } - default: { - break; - } - } + } + + /** + * called when the 'refreshParent' event from ListItemFormComponent is triggered. + * + * @param data info about the operation that was performed on the node and should be reflected in the UI. + * @param firstNode states whether the node is a new child node; defaults to false. + */ + updateView(data: ListNodeOperation, firstNode = false) { + // update the view by updating the existing list + if (data instanceof ListNodeOperation) { + switch (data.operation) { + case 'create': { + if (firstNode) { + // in case of new child node, we have to use the children from list + const index: number = this.list.findIndex( + (item) => item.id === this.expandedNode + ); + this.list[index].children.push(data.listNode); + } else { + this.list.push(data.listNode); + } + break; } + case 'insert': { + // get the corresponding list from the API again and reassign the local list with its response + this._listApiService + .get(this.parentIri) + .subscribe(response => { + if (response instanceof ListResponse) { + this.list = response.list.children; // root node + } else { + this.list = response.node.children; // child node + } + + // emit the updated list of children to the parent node + this.refreshChildren.emit(this.list); + this._cd.markForCheck(); + }); + break; + } + case 'update': { + // use the position from the response from DSP-API to find the correct node to update + this.list[data.listNode.position].labels = + data.listNode.labels; + this.list[data.listNode.position].comments = + data.listNode.comments; + break; + } + case 'delete': { + // conveniently, the response returned by DSP-API contains the entire list without the deleted node within its 'children' + // we can just reassign the list to this + this.list = data.listNode.children; + + // emit the updated list of children to the parent node + this.refreshChildren.emit(this.list); + break; + } + case 'reposition': { + const repositionRequest: RepositionChildNodeRequest = + new RepositionChildNodeRequest(); + repositionRequest.parentNodeIri = this.parentIri; + repositionRequest.position = data.listNode.position; + + // since we don't have any way to know the parent IRI from the ListItemForm component, we need to do the API call here + // --> TODO now we have the parent IRI within the ListItemForm component so we can move this logic there + this._listApiService + .repositionChildNode( + data.listNode.id, + repositionRequest + ) + .subscribe(response => { + this.list = response.node.children; + this.refreshChildren.emit(this.list); + this._cd.markForCheck(); + } + ); + break; + } + default: { + break; + } + } } - - /** - * updates the children of the parent node - * - * @param children the updated list of children nodes - * @param position the position of the parent node - */ - updateParentNodeChildren(children: ListNode[], position: number) { - this.list[position].children = children; - } + } + + /** + * updates the children of the parent node + * + * @param children the updated list of children nodes + * @param position the position of the parent node + */ + updateParentNodeChildren(children: ListNode[], position: number) { + this.list[position].children = children; + } } 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 84ddaeb480..afd1c4c76f 100644 --- a/apps/dsp-app/src/app/project/list/list.component.ts +++ b/apps/dsp-app/src/app/project/list/list.component.ts @@ -1,21 +1,31 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + HostListener, + OnDestroy, + OnInit, +} from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; -import { - ListNodeInfo, - StringLiteral, -} from '@dasch-swiss/dsp-js'; +import { ListNodeInfo, StringLiteral } from '@dasch-swiss/dsp-js'; import { AppGlobal } from '@dsp-app/src/app/app-global'; -import {AppConfigService, RouteConstants} from '@dasch-swiss/vre/shared/app-config'; +import { + AppConfigService, + RouteConstants, +} from '@dasch-swiss/vre/shared/app-config'; import { DialogComponent } from '@dsp-app/src/app/main/dialog/dialog.component'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { ProjectBase } from '../project-base'; -import { Actions, Select, Store, ofActionSuccessful } from '@ngxs/store'; +import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; import { Observable, Subject } from 'rxjs'; import { map, take } from 'rxjs/operators'; -import { DeleteListNodeAction, ListsSelectors, LoadListsInProjectAction, ProjectsSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { + DeleteListNodeAction, + ListsSelectors, + LoadListsInProjectAction, ProjectsSelectors, +} from '@dasch-swiss/vre/shared/app-state'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -25,7 +35,7 @@ import { DeleteListNodeAction, ListsSelectors, LoadListsInProjectAction, Project }) export class ListComponent extends ProjectBase implements OnInit, OnDestroy { private ngUnsubscribe: Subject = new Subject(); - + languagesList: StringLiteral[] = AppGlobal.languagesList; // current selected language @@ -50,19 +60,20 @@ export class ListComponent extends ProjectBase implements OnInit, OnDestroy { get list$(): Observable { return this.listsInProject$.pipe( - map(lists => this.listIri - ? lists.find((i) => i.id === this.listIri) - : null - )); + map((lists) => + this.listIri ? lists.find((i) => i.id === this.listIri) : null + ) + ); } - + @Select(ListsSelectors.isListsLoading) isListsLoading$: Observable; - @Select(ListsSelectors.listsInProject) listsInProject$: Observable; + @Select(ListsSelectors.listsInProject) listsInProject$: Observable< + ListNodeInfo[] + >; constructor( private _acs: AppConfigService, private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, protected _route: ActivatedRoute, protected _router: Router, protected _titleService: Title, @@ -71,7 +82,15 @@ export class ListComponent extends ProjectBase implements OnInit, OnDestroy { protected _cd: ChangeDetectorRef, protected _actions$: Actions ) { - super(_store, _route, _projectService, _titleService, _router, _cd, _actions$); + super( + _store, + _route, + _projectService, + _titleService, + _router, + _cd, + _actions$ + ); } @HostListener('window:resize', ['$event']) onWindowResize() { @@ -89,7 +108,6 @@ export class ListComponent extends ProjectBase implements OnInit, OnDestroy { // set the page title this._setPageTitle(); - // get list iri from list name this._route.params.subscribe((params) => { if (this.project) { @@ -133,13 +151,24 @@ export class ListComponent extends ProjectBase implements OnInit, OnDestroy { } case 'deleteList': { if (typeof data === 'boolean' && data === true) { - this._store.dispatch(new DeleteListNodeAction(this.listIri)); + this._store.dispatch( + new DeleteListNodeAction(this.listIri) + ); this.listIri = undefined; - this._actions$.pipe(ofActionSuccessful(DeleteListNodeAction)) + this._actions$ + .pipe(ofActionSuccessful(DeleteListNodeAction)) .pipe(take(1)) .subscribe(() => { - this._store.dispatch(new LoadListsInProjectAction(this.projectIri)); - this._router.navigate([RouteConstants.project, this.projectUuid,RouteConstants.dataModels]); + this._store.dispatch( + new LoadListsInProjectAction( + this.projectIri + ) + ); + this._router.navigate([ + RouteConstants.project, + this.projectUuid, + RouteConstants.dataModels, + ]); }); } break; @@ -149,7 +178,11 @@ export class ListComponent extends ProjectBase implements OnInit, OnDestroy { } private _setPageTitle() { - const project = this._store.selectSnapshot(ProjectsSelectors.currentProject); - this._titleService.setTitle(`Project ${project?.shortname} | List ${this.listIri ? '' : 's'}`); + const project = this._store.selectSnapshot( + ProjectsSelectors.currentProject + ); + this._titleService.setTitle( + `Project ${project?.shortname} | List${this.listIri ? '' : 's'}` + ); } } diff --git a/apps/dsp-app/src/app/project/project-base.ts b/apps/dsp-app/src/app/project/project-base.ts index f894ff97a3..b5acf1ec3d 100644 --- a/apps/dsp-app/src/app/project/project-base.ts +++ b/apps/dsp-app/src/app/project/project-base.ts @@ -17,7 +17,7 @@ export class ProjectBase implements OnInit, OnDestroy { // permissions of logged-in user get isAdmin$(): Observable { - return combineLatest([this.user$, this.userProjectAdminGroups$, this._route.params, this._route.parent.params]) + return combineLatest([this.user$.pipe(filter(user => user !== null)), this.userProjectAdminGroups$, this._route.params, this._route.parent.params]) .pipe( takeUntil(this.destroyed), map(([user, userProjectGroups, params, parentParams]) => { @@ -30,14 +30,14 @@ export class ProjectBase implements OnInit, OnDestroy { get projectIri() { return this._projectService.uuidToIri(this.projectUuid); } - + @Select(UserSelectors.user) user$: Observable; @Select(UserSelectors.userProjectAdminGroups) userProjectAdminGroups$: Observable; @Select(ProjectsSelectors.isCurrentProjectAdmin) isProjectAdmin$: Observable; @Select(ProjectsSelectors.isCurrentProjectMember) isProjectMember$: Observable; @Select(ProjectsSelectors.currentProject) project$: Observable; @Select(ProjectsSelectors.isProjectsLoading) isProjectsLoading$: Observable; - + constructor( protected _store: Store, protected _route: ActivatedRoute, @@ -48,7 +48,7 @@ export class ProjectBase implements OnInit, OnDestroy { protected _actions$: Actions, ) { // get the uuid of the current project - this.projectUuid = this._route.snapshot.params.uuid + this.projectUuid = this._route.snapshot.params.uuid ? this._route.snapshot.params.uuid : this._route.parent.snapshot.params.uuid; } @@ -83,7 +83,7 @@ export class ProjectBase implements OnInit, OnDestroy { return projects.find(x => x.id.split('/').pop() === this.projectUuid); } - + private loadProject(): void { this._store.dispatch(new LoadProjectAction(this.projectUuid, true)); this._actions$.pipe(ofActionSuccessful(LoadProjectAction)) @@ -113,14 +113,14 @@ export class ProjectBase implements OnInit, OnDestroy { let result = true; this.project.ontologies.forEach((ontoIri) => { - if (!currentProjectOntologies - || currentProjectOntologies.length === 0 + if (!currentProjectOntologies + || currentProjectOntologies.length === 0 || !currentProjectOntologies.find((o) => o.id === ontoIri) ) { result = false; } }); - + return result; } } diff --git a/apps/dsp-app/src/app/project/project-form/project-form.component.ts b/apps/dsp-app/src/app/project/project-form/project-form.component.ts index b2d8fe363c..a168496db0 100644 --- a/apps/dsp-app/src/app/project/project-form/project-form.component.ts +++ b/apps/dsp-app/src/app/project/project-form/project-form.component.ts @@ -1,495 +1,479 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Inject, - Input, - OnInit -} from '@angular/core'; -import { - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, - Validators, -} from '@angular/forms'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; -import {ActivatedRoute, Params, Router} from '@angular/router'; -import { Location } from "@angular/common"; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { Location } from '@angular/common'; import { - ApiResponseData, ApiResponseError, KnoraApiConnection, Project, - ProjectResponse, - ProjectsResponse, ReadProject, ReadUser, StringLiteral, - UpdateProjectRequest, + UpdateProjectRequest } from '@dasch-swiss/dsp-js'; -import {DspApiConnectionToken, RouteConstants} from '@dasch-swiss/vre/shared/app-config'; +import { DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { existingNamesValidator } from '@dsp-app/src/app/main/directive/existing-name/existing-name.directive'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; -import { Actions, Store, ofActionSuccessful } from '@ngxs/store'; -import { LoadProjectsAction, ProjectsSelectors, UpdateProjectAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; +import { + LoadProjectsAction, ProjectsSelectors, + UpdateProjectAction, + UserSelectors +} from '@dasch-swiss/vre/shared/app-state'; +import { ProjectApiService, UserApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-project-form', - templateUrl: './project-form.component.html', - styleUrls: ['./project-form.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-project-form', + templateUrl: './project-form.component.html', + styleUrls: ['./project-form.component.scss'] }) export class ProjectFormComponent implements OnInit { - /** - * param of project form component: - * Optional projectIri; if exists we are in edit mode - * otherwise we build empty form to create new project - */ - @Input() projectIri?: string; - - project: ReadProject; - projectUuid: string; - description: StringLiteral[]; - - loading = true; - - /** - * shortcode and shortname must be unique - */ - existingShortNames: [RegExp] = [ - new RegExp('anEmptyRegularExpressionWasntPossible'), - ]; - shortnameRegex = /^[a-zA-Z]+\S*$/; - - existingShortcodes: [RegExp] = [ - new RegExp('anEmptyRegularExpressionWasntPossible'), - ]; - shortcodeRegex = /^[0-9A-Fa-f]+$/; - - /** - * some restrictions and rules for - * description, shortcode, shortname and keywords - */ - descriptionMaxLength = 2000; - shortcodeMinLength = 4; - shortcodeMaxLength: number = this.shortcodeMinLength; - - shortnameMinLength = 3; - shortnameMaxLength = 20; - - // keywords is an array of objects of {name: 'string'} - keywords: string[] = []; - // separator: Enter, comma - separatorKeyCodes = [ENTER, COMMA]; - visible = true; - selectable = true; - removable = true; - addOnBlur = true; - - /** - * success of sending data - */ - success = false; - - /** - * message after successful post - */ - successMessage: any = { - status: 200, - statusText: 'You have successfully updated the project data.', - }; - - /** - * form group, errors and validation messages - */ - form: UntypedFormGroup; - - formErrors = { - shortname: '', - longname: '', - shortcode: '', - description: '', - keywords: '', - // 'institution': '' - }; - - validationMessages = { - shortname: { - required: 'Short name is required.', - minlength: - 'Short name must be at least ' + - this.shortnameMinLength + - ' characters long.', - maxlength: - 'Short name cannot be more than ' + - this.shortnameMaxLength + - ' characters long.', - pattern: - "Short name shouldn't start with a number; Spaces are not allowed.", - existingName: 'This short name is already taken.', - }, - longname: { - required: 'Project (long) name is required.', - }, - shortcode: { - required: 'Shortcode is required', - maxlength: - 'Shortcode cannot be more than ' + - this.shortcodeMaxLength + - ' characters long.', - minlength: - 'Shortcode cannot be less than ' + - this.shortcodeMinLength + - ' characters long.', - pattern: 'This is not a hexadecimal value!', - existingName: 'This shortcode is already taken.', - }, - description: { - required: 'A description is required.', - maxlength: - 'Description cannot be more than ' + - this.descriptionMaxLength + - ' characters long.', - }, - keywords: { - required: 'At least one keyword is required.', - }, - // 'institution': {} - }; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _notification: NotificationService, - private _fb: UntypedFormBuilder, - private _route: ActivatedRoute, - private _router: Router, - private _location: Location, - private _projectService: ProjectService, - private _store: Store, - private _actions$: Actions, - private _cd: ChangeDetectorRef, - ) { - // get the uuid of the current project - this._route.parent.paramMap.subscribe((params: Params) => { - this.projectUuid = params.get('uuid'); - }); + /** + * param of project form component: + * Optional projectIri; if exists we are in edit mode + * otherwise we build empty form to create new project + */ + @Input() projectIri?: string; + + project: ReadProject; + projectUuid: string; + description: StringLiteral[]; + + loading = true; + + /** + * shortcode and shortname must be unique + */ + existingShortNames: [RegExp] = [ + new RegExp('anEmptyRegularExpressionWasntPossible') + ]; + shortnameRegex = /^[a-zA-Z]+\S*$/; + + existingShortcodes: [RegExp] = [ + new RegExp('anEmptyRegularExpressionWasntPossible') + ]; + shortcodeRegex = /^[0-9A-Fa-f]+$/; + + /** + * some restrictions and rules for + * description, shortcode, shortname and keywords + */ + descriptionMaxLength = 2000; + shortcodeMinLength = 4; + shortcodeMaxLength: number = this.shortcodeMinLength; + + shortnameMinLength = 3; + shortnameMaxLength = 20; + + // keywords is an array of objects of {name: 'string'} + keywords: string[] = []; + // separator: Enter, comma + separatorKeyCodes = [ENTER, COMMA]; + visible = true; + selectable = true; + removable = true; + addOnBlur = true; + + /** + * success of sending data + */ + success = false; + + /** + * message after successful post + */ + successMessage: any = { + status: 200, + statusText: 'You have successfully updated the project data.' + }; + + /** + * form group, errors and validation messages + */ + form: UntypedFormGroup; + + formErrors = { + shortname: '', + longname: '', + shortcode: '', + description: '', + keywords: '' + // 'institution': '' + }; + + validationMessages = { + shortname: { + required: 'Short name is required.', + minlength: + 'Short name must be at least ' + + this.shortnameMinLength + + ' characters long.', + maxlength: + 'Short name cannot be more than ' + + this.shortnameMaxLength + + ' characters long.', + pattern: + 'Short name shouldn\'t start with a number; Spaces are not allowed.', + existingName: 'This short name is already taken.' + }, + longname: { + required: 'Project (long) name is required.' + }, + shortcode: { + required: 'Shortcode is required', + maxlength: + 'Shortcode cannot be more than ' + + this.shortcodeMaxLength + + ' characters long.', + minlength: + 'Shortcode cannot be less than ' + + this.shortcodeMinLength + + ' characters long.', + pattern: 'This is not a hexadecimal value!', + existingName: 'This shortcode is already taken.' + }, + description: { + required: 'A description is required.', + maxlength: + 'Description cannot be more than ' + + this.descriptionMaxLength + + ' characters long.' + }, + keywords: { + required: 'At least one keyword is required.' } - - ngOnInit() { - // if a projectUuid exists, we are in edit mode - // otherwise create new project - if (this.projectUuid) { - this.projectIri = this._projectService.uuidToIri(this.projectUuid) - // edit existing project - // get origin project data first - this._dspApiConnection.admin.projectsEndpoint - .getProjectByIri(this.projectIri) - .subscribe( - (response: ApiResponseData) => { - // save the origin project data in case of reset - this.project = response.body.project; - - this.buildForm(this.project); - - this.loading = false; - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } else { - // create new project - - // to avoid duplicate shortcodes or shortnames - // we have to create a list of already exisiting short codes and names - this._dspApiConnection.admin.projectsEndpoint - .getProjects() - .subscribe( - (response: ApiResponseData) => { - for (const project of response.body.projects) { - this.existingShortNames.push( - new RegExp( - '(?:^|W)' + - project.shortname.toLowerCase() + - '(?:$|W)' - ) - ); - - if (project.shortcode !== null) { - this.existingShortcodes.push( - new RegExp( - '(?:^|W)' + - project.shortcode.toLowerCase() + - '(?:$|W)' - ) - ); - } - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - - this.project = new ReadProject(); - this.project.status = true; + // 'institution': {} + }; + + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _projectApiService: ProjectApiService, + private _userApiService: UserApiService, + private _errorHandler: AppErrorHandler, + private _notification: NotificationService, + private _fb: UntypedFormBuilder, + private _route: ActivatedRoute, + private _router: Router, + private _location: Location, + private _projectService: ProjectService, + private _store: Store, + private _actions$: Actions, + private _cd: ChangeDetectorRef + ) { + // get the uuid of the current project + this._route.parent.paramMap.subscribe((params: Params) => { + this.projectUuid = params.get('uuid'); + }); + } + + ngOnInit() { + // if a projectUuid exists, we are in edit mode + // otherwise create new project + if (this.projectUuid) { + this.projectIri = this._projectService.uuidToIri(this.projectUuid); + // edit existing project + // get origin project data first + this._projectApiService.get(this.projectIri) + .subscribe( + (response) => { + // save the origin project data in case of reset + this.project = response.project; this.buildForm(this.project); this.loading = false; - } - } - - trackByFn = (index: number, item: string) => `${index}-${item}`; - - /** - * build form with project data - * Project data contains exising data (edit mode) - * or no data (create mode) => new ReadProject() - * - * @param project - */ - buildForm(project: ReadProject): void { - // if project is defined, we're in the edit mode - // otherwise "create new project" mode is active - // edit mode is true, when a projectIri exists - - // disabled is true, if project status is false (= archived); - const disabled = !project.status; - - // separate description - if (!this.projectIri) { - this.description = [new StringLiteral()]; - this.formErrors['description'] = ''; - } + this._cd.markForCheck(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } else { + // create new project + + // to avoid duplicate shortcodes or shortnames + // we have to create a list of already exisiting short codes and names + this._projectApiService.list() + .subscribe( + response => { + for (const project of response.projects) { + this.existingShortNames.push( + new RegExp( + '(?:^|W)' + + project.shortname.toLowerCase() + + '(?:$|W)' + ) + ); + + if (project.shortcode !== null) { + this.existingShortcodes.push( + new RegExp( + '(?:^|W)' + + project.shortcode.toLowerCase() + + '(?:$|W)' + ) + ); + } + } + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); - // separate list of keywords - this.keywords = project.keywords; - - this.form = this._fb.group({ - shortname: new UntypedFormControl( - { - value: project.shortname, - disabled: this.projectIri, - }, - [ - Validators.required, - Validators.minLength(this.shortnameMinLength), - Validators.maxLength(this.shortnameMaxLength), - existingNamesValidator(this.existingShortNames), - Validators.pattern(this.shortnameRegex), - ] - ), - longname: new UntypedFormControl( - { - value: project.longname, - disabled: disabled, - }, - [Validators.required] - ), - shortcode: new UntypedFormControl( - { - value: project.shortcode, - disabled: this.projectIri && project.shortcode !== null, - }, - [ - Validators.required, - Validators.minLength(this.shortcodeMinLength), - Validators.maxLength(this.shortcodeMaxLength), - existingNamesValidator(this.existingShortcodes), - Validators.pattern(this.shortcodeRegex), - ] - ), - logo: new UntypedFormControl({ - value: project.logo, - disabled: disabled, - }), - status: [true], - selfjoin: [false], - keywords: new UntypedFormControl({ - // must be empty (even in edit mode), because of the mat-chip-list - value: [], - disabled: disabled, - }), - }); + this.project = new ReadProject(); + this.project.status = true; - if (!this.projectIri) { - // if projectIri does not exist, we are in create mode; - // in this case, the keywords are required in the API request - this.form.controls['keywords'].setValidators(Validators.required); - } + this.buildForm(this.project); - this.form.valueChanges.subscribe(() => this.onValueChanged()); + this.loading = false; + } + } + + trackByFn = (index: number, item: string) => `${index}-${item}`; + + /** + * build form with project data + * Project data contains exising data (edit mode) + * or no data (create mode) => new ReadProject() + * + * @param project + */ + buildForm(project: ReadProject): void { + // if project is defined, we're in the edit mode + // otherwise "create new project" mode is active + // edit mode is true, when a projectIri exists + + // disabled is true, if project status is false (= archived); + const disabled = !project.status; + + // separate description + if (!this.projectIri) { + this.description = [new StringLiteral()]; + this.formErrors['description'] = ''; } - /** - * this method is for the form error handling - */ - onValueChanged() { - if (!this.form) { - return; - } - - const form = this.form; + // separate list of keywords + this.keywords = project.keywords; - Object.keys(this.formErrors).map((field) => { - this.formErrors[field] = ''; - const control = form.get(field); - if (control && control.dirty && !control.valid) { - const messages = this.validationMessages[field]; - Object.keys(control.errors).map((key) => { - this.formErrors[field] += messages[key] + ' '; - }); - } - }); + this.form = this._fb.group({ + shortname: new UntypedFormControl( + { + value: project.shortname, + disabled: this.projectIri + }, + [ + Validators.required, + Validators.minLength(this.shortnameMinLength), + Validators.maxLength(this.shortnameMaxLength), + existingNamesValidator(this.existingShortNames), + Validators.pattern(this.shortnameRegex) + ] + ), + longname: new UntypedFormControl( + { + value: project.longname, + disabled: disabled + }, + [Validators.required] + ), + shortcode: new UntypedFormControl( + { + value: project.shortcode, + disabled: this.projectIri && project.shortcode !== null + }, + [ + Validators.required, + Validators.minLength(this.shortcodeMinLength), + Validators.maxLength(this.shortcodeMaxLength), + existingNamesValidator(this.existingShortcodes), + Validators.pattern(this.shortcodeRegex) + ] + ), + logo: new UntypedFormControl({ + value: project.logo, + disabled: disabled + }), + status: [true], + selfjoin: [false], + keywords: new UntypedFormControl({ + // must be empty (even in edit mode), because of the mat-chip-list + value: [], + disabled: disabled + }) + }); + + if (!this.projectIri) { + // if projectIri does not exist, we are in create mode; + // in this case, the keywords are required in the API request + this.form.controls['keywords'].setValidators(Validators.required); } - /** - * gets string literal - * @param data - */ - getStringLiteral(data: StringLiteral[]) { - this.description = data; - if (!this.description.length) { - this.formErrors['description'] = - this.validationMessages['description'].required; - } else { - this.formErrors['description'] = ''; - } + this.form.valueChanges.subscribe(() => this.onValueChanged()); + } + + /** + * this method is for the form error handling + */ + onValueChanged() { + if (!this.form) { + return; } - addKeyword(event: MatChipInputEvent): void { - const input = event.input; - const value = event.value; + const form = this.form; - if (!this.keywords) { - this.keywords = []; - } + Object.keys(this.formErrors).map((field) => { + this.formErrors[field] = ''; + const control = form.get(field); + if (control && control.dirty && !control.valid) { + const messages = this.validationMessages[field]; + Object.keys(control.errors).map((key) => { + this.formErrors[field] += messages[key] + ' '; + }); + } + }); + } + + /** + * gets string literal + * @param data + */ + getStringLiteral(data: StringLiteral[]) { + this.description = data; + if (!this.description.length) { + this.formErrors['description'] = + this.validationMessages['description'].required; + } else { + this.formErrors['description'] = ''; + } + } - // add keyword - if ((value || '').trim()) { - this.keywords.push(value.trim()); - } + addKeyword(event: MatChipInputEvent): void { + const input = event.input; + const value = event.value; - // reset the input value - if (input) { - input.value = ''; - } + if (!this.keywords) { + this.keywords = []; } - removeKeyword(keyword: any): void { - const index = this.keywords.indexOf(keyword); + // add keyword + if ((value || '').trim()) { + this.keywords.push(value.trim()); + } - if (index >= 0) { - this.keywords.splice(index, 1); - } + // reset the input value + if (input) { + input.value = ''; } + } - submitData() { - this.loading = true; + removeKeyword(keyword: any): void { + const index = this.keywords.indexOf(keyword); - // a) update keywords from mat-chip-list - if (!this.keywords) { - this.keywords = []; - } - this.form.controls['keywords'].setValue(this.keywords); - - if (this.projectIri) { - const projectData = new UpdateProjectRequest(); - projectData.description = [new StringLiteral()]; - projectData.keywords = this.form.value.keywords; - projectData.longname = this.form.value.longname; - projectData.status = true; - - let i = 0; - for (const d of this.description) { - projectData.description[i] = new StringLiteral(); - projectData.description[i].language = d.language; - projectData.description[i].value = d.value; - i++; - } + if (index >= 0) { + this.keywords.splice(index, 1); + } + } - // edit / update project data - this._store.dispatch(new UpdateProjectAction(this.project.id, projectData)); - this._actions$.pipe(ofActionSuccessful(LoadProjectsAction)) - .subscribe(() => { - this.success = true; - this.project = this._store.selectSnapshot(ProjectsSelectors.currentProject); - this._notification.openSnackBar('You have successfully updated the project information.'); - this._router.navigate([`${RouteConstants.projectRelative}/${this.projectUuid}`]) - this.loading = false; - } - ); - } else { - // create new project - const projectData: Project = new Project(); - - projectData.shortcode = this.form.value.shortcode; - projectData.shortname = this.form.value.shortname; - projectData.longname = this.form.value.longname; - projectData.keywords = this.form.value.keywords; - projectData.description = [new StringLiteral()]; - projectData.status = true; - projectData.selfjoin = false; - - let i = 0; - for (const d of this.description) { - projectData.description[i] = new StringLiteral(); - projectData.description[i].language = d.language; - projectData.description[i].value = d.value; - i++; - } + submitData() { + this.loading = true; - this._dspApiConnection.admin.projectsEndpoint - .createProject(projectData) - .subscribe( - (projectResponse: ApiResponseData) => { - this.project = projectResponse.body.project; - this.buildForm(this.project); - - // add logged-in user to the project - // who am I? - const user = this._store.selectSnapshot(UserSelectors.user) as ReadUser; - this._dspApiConnection.admin.usersEndpoint - .addUserToProjectMembership( - user.id, - projectResponse.body.project.id - ) - .subscribe( - () => { - const uuid = this._projectService.iriToUuid(projectResponse.body.project.id); - this.loading = false; - // redirect to project page - this._router.navigateByUrl(`${RouteConstants.projectRelative}`, { - skipLocationChange: - true, - }) - .then(() => - this._router.navigate([`${RouteConstants.projectRelative}/${uuid}`]) - ); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage( - error - ); - } - ); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - this.loading = false; - } - ); + // a) update keywords from mat-chip-list + if (!this.keywords) { + this.keywords = []; + } + this.form.controls['keywords'].setValue(this.keywords); + + if (this.projectIri) { + const projectData = new UpdateProjectRequest(); + projectData.description = [new StringLiteral()]; + projectData.keywords = this.form.value.keywords; + projectData.longname = this.form.value.longname; + projectData.status = true; + + let i = 0; + for (const d of this.description) { + projectData.description[i] = new StringLiteral(); + projectData.description[i].language = d.language; + projectData.description[i].value = d.value; + i++; + } + + // edit / update project data + this._store.dispatch(new UpdateProjectAction(this.project.id, projectData)); + this._actions$.pipe(ofActionSuccessful(LoadProjectsAction)) + .subscribe(() => { + + this.success = true; + this.project = + this._store.selectSnapshot(ProjectsSelectors.currentProject); + this._notification.openSnackBar('You have successfully updated the project information.'); + this._router.navigate([`${RouteConstants.projectRelative}/${this.projectUuid}`]); + this.loading = false; + } + ); + } else { + // create new project + const projectData: Project = new Project(); + + projectData.shortcode = this.form.value.shortcode; + projectData.shortname = this.form.value.shortname; + projectData.longname = this.form.value.longname; + projectData.keywords = this.form.value.keywords; + projectData.description = [new StringLiteral()]; + projectData.status = true; + projectData.selfjoin = false; + + let i = 0; + for (const d of this.description) { + projectData.description[i] = new StringLiteral(); + projectData.description[i].language = d.language; + projectData.description[i].value = d.value; + i++; + } + + this._projectApiService.create(projectData).subscribe( + projectResponse => { + this.project = projectResponse.project; + this.buildForm(this.project); + + // add logged-in user to the project + // who am I? + const user = this._store.selectSnapshot(UserSelectors.user) as ReadUser; + this._userApiService + .addToProjectMembership( + user.id, + projectResponse.project.id + ) + .subscribe( + () => { + const uuid = this._projectService.iriToUuid(projectResponse.project.id); + this.loading = false; + // redirect to project page + this._router.navigateByUrl(`${RouteConstants.projectRelative}`, { + skipLocationChange: + true + }) + .then(() => + this._router.navigate([`${RouteConstants.projectRelative}/${uuid}`]) + ); + }); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + this.loading = false; } + ); } + } - goBack(): void { - this._location.back(); - } + goBack(): void { + this._location.back(); + } } 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 9f90b3fda9..6c4fc2c134 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 @@ -9,215 +9,207 @@ import { Output } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; -import {Router} from '@angular/router'; -import { - Constants, - KnoraApiConnection, - ReadProject, - ReadUser, - StoredProject, - UpdateProjectRequest, -} from '@dasch-swiss/dsp-js'; -import {DspApiConnectionToken, RouteConstants} from '@dasch-swiss/vre/shared/app-config'; +import { Router } from '@angular/router'; +import { Constants, ReadProject, ReadUser, StoredProject, UpdateProjectRequest } from '@dasch-swiss/dsp-js'; +import { DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { DialogComponent } from '@dsp-app/src/app/main/dialog/dialog.component'; import { SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import {SortProp} from "@dsp-app/src/app/main/action/sort-button/sort-button.component"; import {Observable, Subject, combineLatest} from "rxjs"; -import {map, take, takeUntil} from "rxjs/operators"; +import { map, take, takeUntil, tap } from 'rxjs/operators'; import { Select } from '@ngxs/store'; import { ProjectsSelectors, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-projects-list', - templateUrl: './projects-list.component.html', - styleUrls: ['./projects-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-projects-list', + templateUrl: './projects-list.component.html', + styleUrls: ['./projects-list.component.scss'] }) export class ProjectsListComponent implements OnInit, OnDestroy { - private ngUnsubscribe: Subject = new Subject(); - - // list of users: status active or inactive (deleted) - @Input() status: boolean; + private ngUnsubscribe: Subject = new Subject(); - // list of projects: depending on the parent - @Input() list: StoredProject[]; + // list of users: status active or inactive (deleted) + @Input() status: boolean; - // enable the button to create new project - @Input() createNew = false; + // list of projects: depending on the parent + @Input() list: StoredProject[]; - // in case of modification - @Output() refreshParent: EventEmitter = new EventEmitter(); + // enable the button to create new project + @Input() createNew = false; - // loading for progess indicator - loading: boolean; + // in case of modification + @Output() refreshParent: EventEmitter = new EventEmitter(); - // list of default, dsp-specific projects, which are not able to be deleted or to be editied - doNotDelete: string[] = [ - Constants.SystemProjectIRI, - Constants.DefaultSharedOntologyIRI, - ]; + // loading for progess indicator + loading: boolean; - // i18n plural mapping - itemPluralMapping = { - project: { - // eslint-disable-next-line @typescript-eslint/naming-convention - '=1': '1 Project', - other: '# Projects', - }, - }; - - // sort properties - sortProps: SortProp[] = [ - { - key: 'shortcode', - label: 'Short code', - }, - { - key: 'shortname', - label: 'Short name', - }, - { - key: 'longname', - label: 'Project name', - }, - ]; - - sortBy = 'longname'; // default sort by - - @Select(UserSelectors.user) user$: Observable; - @Select(UserSelectors.userProjectAdminGroups) userProjectAdminGroups$: Observable; - @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; - @Select(ProjectsSelectors.readProjects) readProjects$: Observable; - @Select(ProjectsSelectors.isProjectsLoading) isProjectsLoading$: Observable; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _dialog: MatDialog, - private _router: Router, - private _sortingService: SortingService, - ) {} - - ngOnInit() { - // sort list by defined key - this.sortBy = localStorage.getItem('sortProjectsBy') || this.sortBy; - this.sortList(this.sortBy); - } + // list of default, dsp-specific projects, which are not able to be deleted or to be editied + doNotDelete: string[] = [ + Constants.SystemProjectIRI, + Constants.DefaultSharedOntologyIRI + ]; - ngOnDestroy() { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); + // i18n plural mapping + itemPluralMapping = { + project: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '=1': '1 Project', + other: '# Projects' } - - /** - * return true, when the user is entitled to edit a project. This is - * the case when a user either system admin or project admin of the given project. - * - * @param projectId the iri of the project to be checked - */ - userHasPermission$(projectIri: string): Observable { - return combineLatest([this.user$, this.userProjectAdminGroups$]) - .pipe( - takeUntil(this.ngUnsubscribe), - map(([user, userProjectGroups]) => { - return ProjectService.IsProjectAdminOrSysAdmin(user, userProjectGroups, projectIri); - }) - ) - } - - /** - * return true, when the user is project admin of the given project. - * - * @param projectIri the iri of the project to be checked - */ - userIsProjectAdmin$(projectIri: string): Observable { - return combineLatest([this.user$, this.userProjectAdminGroups$]) - .pipe( - takeUntil(this.ngUnsubscribe), - map(([user, userProjectGroups]) => { - return ProjectService.IsInProjectGroup(userProjectGroups, projectIri); - }) - ) - } - - /** - * navigate to the project pages (e.g. board, collaboration or ontology) - * - * @param iri - */ - openProjectPage(iri: string) { - const uuid = ProjectService.IriToUuid(iri); - - this._router - .navigateByUrl(`/${RouteConstants.refresh}`, { skipLocationChange: true }) - .then(() => this._router.navigate([RouteConstants.project, uuid])); - } - - createNewProject() { - this._router.navigate([RouteConstants.project, RouteConstants.createNew]); + }; + + // sort properties + sortProps: SortProp[] = [ + { + key: 'shortcode', + label: 'Short code' + }, + { + key: 'shortname', + label: 'Short name' + }, + { + key: 'longname', + label: 'Project name' } + ]; + + sortBy = 'longname'; // default sort by + + @Select(UserSelectors.user) user$: Observable; + @Select(UserSelectors.userProjectAdminGroups) userProjectAdminGroups$: Observable; + @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; + @Select(ProjectsSelectors.readProjects) readProjects$: Observable; + @Select(ProjectsSelectors.isProjectsLoading) isProjectsLoading$: Observable; + + constructor( + private _projectApiService: ProjectApiService, + private _dialog: MatDialog, + private _router: Router, + private _sortingService: SortingService, + ) { + } + + ngOnInit() { + // sort list by defined key + this.sortBy = localStorage.getItem('sortProjectsBy') || this.sortBy; + this.sortList(this.sortBy); + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + /** + * return true, when the user is entitled to edit a project. This is + * the case when a user either system admin or project admin of the given project. + * + * @param projectId the iri of the project to be checked + */ + userHasPermission$(projectIri: string): Observable { + return combineLatest([this.user$, this.userProjectAdminGroups$]) + .pipe( + takeUntil(this.ngUnsubscribe), + map(([user, userProjectGroups]) => { + return ProjectService.IsProjectAdminOrSysAdmin(user, userProjectGroups, projectIri); + }) + ); + } + + /** + * return true, when the user is project admin of the given project. + * + * @param projectIri the iri of the project to be checked + */ + userIsProjectAdmin$(projectIri: string): Observable { + return combineLatest([this.user$, this.userProjectAdminGroups$]) + .pipe( + takeUntil(this.ngUnsubscribe), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + map(([user, userProjectGroups]) => { + return ProjectService.IsInProjectGroup(userProjectGroups, projectIri); + }) + ); + } + + /** + * navigate to the project pages (e.g. board, collaboration or ontology) + * + * @param iri + */ + openProjectPage(iri: string) { + const uuid = ProjectService.IriToUuid(iri); + + this._router + .navigateByUrl(`/${RouteConstants.refresh}`, { skipLocationChange: true }) + .then(() => this._router.navigate([RouteConstants.project, uuid])); + } + + createNewProject() { + this._router.navigate([RouteConstants.project, RouteConstants.createNew]); + } + + editProject(iri: string) { + const uuid = ProjectService.IriToUuid(iri); + this._router.navigate([RouteConstants.project, uuid, RouteConstants.edit]); + } + + openDialog(mode: string, name?: string, id?: string): void { + const dialogConfig: MatDialogConfig = { + width: '560px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { name: name, mode: mode, project: id } + }; - editProject(iri: string) { - const uuid = ProjectService.IriToUuid(iri); - this._router.navigate([RouteConstants.project, uuid, RouteConstants.edit]); - } + const dialogRef = this._dialog.open(DialogComponent, dialogConfig); - openDialog(mode: string, name?: string, id?: string): void { - const dialogConfig: MatDialogConfig = { - width: '560px', - maxHeight: '80vh', - position: { - top: '112px', - }, - data: { name: name, mode: mode, project: id }, - }; - - const dialogRef = this._dialog.open(DialogComponent, dialogConfig); - - dialogRef.afterClosed().subscribe((response) => { - if (response === true) { - // get the mode - switch (mode) { - case 'deactivateProject': - this.deactivateProject(id); - break; - - case 'activateProject': - this.activateProject(id); - break; - } - } - }); - } + dialogRef.afterClosed().subscribe((response) => { + if (response === true) { + // get the mode + switch (mode) { + case 'deactivateProject': + this.deactivateProject(id); + break; - sortList(key: any) { - if (!this.list) { // guard - return; + case 'activateProject': + this.activateProject(id); + break; } - this.list = this._sortingService.keySortByAlphabetical(this.list, key); - localStorage.setItem('sortProjectsBy', key); - } - - deactivateProject(id: string) { - // the deleteProject() method in js-lib sets the project's status to false, it is not actually deleted - this._dspApiConnection.admin.projectsEndpoint.deleteProject(id) - .pipe(take(1)) - .subscribe(response => { - this.refreshParent.emit(); //TODO Soft or Hard refresh ? - }); - } + } + }); + } - 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._dspApiConnection.admin.projectsEndpoint - .updateProject(id, data) - .pipe(take(1)) - .subscribe(response => { - this.refreshParent.emit(); //TODO Soft or Hard refresh ? - }); + sortList(key: any) { + if (!this.list) { // guard + return; } + this.list = this._sortingService.keySortByAlphabetical(this.list, key); + localStorage.setItem('sortProjectsBy', key); + } + + deactivateProject(id: string) { + this._projectApiService.delete(id) + .pipe(tap(() => { + this.refreshParent.emit(); //TODO Soft or Hard refresh ? + })); + } + + 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(); + })); + } } 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 6f51682f84..622dbc4365 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 @@ -1,434 +1,435 @@ import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Inject, - Input, - OnInit, - Output, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnInit, + Output } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { - ApiResponseData, - ApiResponseError, - Constants, - GroupsResponse, - KnoraApiConnection, - Permissions, - ReadProject, - ReadUser, - UserResponse, + ApiResponseError, + Constants, + KnoraApiConnection, + Permissions, + ReadProject, + ReadUser } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { DialogComponent } from '@dsp-app/src/app/main/dialog/dialog.component'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; -import { SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; -import { Actions, Select, Store, ofActionSuccessful } from '@ngxs/store'; -import { LoadProjectAction, LoadProjectMembersAction, LoadUserAction, ProjectsSelectors, RemoveUserFromProjectAction, SetUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { ProjectService, SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; +import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; +import { + + LoadProjectAction, + LoadProjectMembersAction, + LoadUserAction, + ProjectsSelectors, RemoveUserFromProjectAction, + SetUserAction, + UserSelectors +} from '@dasch-swiss/vre/shared/app-state'; import { Observable } from 'rxjs'; +import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; import { map, take } from 'rxjs/operators'; -import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-users-list', - templateUrl: './users-list.component.html', - styleUrls: ['./users-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-users-list', + templateUrl: './users-list.component.html', + styleUrls: ['./users-list.component.scss'] }) export class UsersListComponent implements OnInit { - // list of users: status active or inactive (deleted) - @Input() status: boolean; - - // list of users: depending on the parent - @Input() list: ReadUser[]; - - // enable the button to create new user - @Input() createNew = false; - - // proje0ct data - @Input() project: ReadProject; - - // in case of modification - @Output() refreshParent: EventEmitter = new EventEmitter(); - - // i18n plural mapping - itemPluralMapping = { - user: { - // eslint-disable-next-line @typescript-eslint/naming-convention - '=1': '1 User', - other: '# Users', - }, - member: { - // eslint-disable-next-line @typescript-eslint/naming-convention - '=1': '1 Member', - other: '# Members', - }, - }; - - // - // project view - // dsp-js admin group iri - adminGroupIri: string = Constants.ProjectAdminGroupIRI; - - // project uuid; as identifier in project application state service - projectUuid: string; - - // - // sort properties - sortProps: any = [ - { - key: 'familyName', - label: 'Last name', - }, - { - key: 'givenName', - label: 'First name', - }, - { - key: 'email', - label: 'E-mail', - }, - { - key: 'username', - label: 'Username', - }, - ]; - - // ... and sort by 'username' - sortBy = 'username'; - - @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; - @Select(UserSelectors.user) user$: Observable; - @Select(UserSelectors.username) username$: Observable; - @Select(ProjectsSelectors.isCurrentProjectAdmin) isProjectAdmin$: Observable; - @Select(ProjectsSelectors.currentProject) project$: Observable; - @Select(ProjectsSelectors.isProjectsLoading) isProjectsLoading$: Observable; - @Select(UserSelectors.isLoading) isUsersLoading$: Observable; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, - private _route: ActivatedRoute, - private _router: Router, - private _sortingService: SortingService, - private _store: Store, - private _projectService: ProjectService, - private _actions$: Actions, - private _cd: ChangeDetectorRef, - ) { - // get the uuid of the current project - this._route.parent.parent.paramMap.subscribe((params: Params) => { - this.projectUuid = params.get('uuid'); - }); + // list of users: status active or inactive (deleted) + @Input() status: boolean; + + // list of users: depending on the parent + @Input() list: ReadUser[]; + + // enable the button to create new user + @Input() createNew = false; + + // proje0ct data + @Input() project: ReadProject; + + // in case of modification + @Output() refreshParent: EventEmitter = new EventEmitter(); + + // i18n plural mapping + itemPluralMapping = { + user: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '=1': '1 User', + other: '# Users' + }, + member: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '=1': '1 Member', + other: '# Members' } - - ngOnInit() { - // sort list by defined key - if (localStorage.getItem('sortUsersBy')) { - this.sortBy = localStorage.getItem('sortUsersBy'); - } else { - localStorage.setItem('sortUsersBy', this.sortBy); - } + }; + + // + // project view + // dsp-js admin group iri + adminGroupIri: string = Constants.ProjectAdminGroupIRI; + + // project uuid; as identifier in project application state service + projectUuid: string; + + // + // sort properties + sortProps: any = [ + { + key: 'familyName', + label: 'Last name' + }, + { + key: 'givenName', + label: 'First name' + }, + { + key: 'email', + label: 'E-mail' + }, + { + key: 'username', + label: 'Username' } - - trackByFn = (index: number, item: ReadUser) => `${index}-${item.id}`; - - /** - * returns true, when the user is project admin; - * when the parameter permissions is not set, - * it returns the value for the logged-in user - * - * - * @param [permissions] user's permissions - * @returns boolean - */ - userIsProjectAdmin(permissions?: Permissions): boolean { - if (!this.project) { - return false; - } - - return ProjectService.IsMemberOfProjectAdminGroup(permissions.groupsPerProject, this.project.id); + ]; + + // ... and sort by 'username' + sortBy = 'username'; + + @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; + @Select(UserSelectors.user) user$: Observable; + @Select(UserSelectors.username) username$: Observable; + @Select(ProjectsSelectors.isCurrentProjectAdmin) isProjectAdmin$: Observable; + @Select(ProjectsSelectors.currentProject) project$: Observable; + @Select(ProjectsSelectors.isProjectsLoading) isProjectsLoading$: Observable; + @Select(UserSelectors.isLoading) isUsersLoading$: Observable; + + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _userApiService: UserApiService, + private _dialog: MatDialog, + private _errorHandler: AppErrorHandler, + private _route: ActivatedRoute, + private _router: Router, + private _sortingService: SortingService, + private _store: Store, + private _projectService: ProjectService, + private _actions$: Actions, + private _cd: ChangeDetectorRef + ) { + // get the uuid of the current project + this._route.parent.parent.paramMap.subscribe((params: Params) => { + this.projectUuid = params.get('uuid'); + }); + } + + ngOnInit() { + // sort list by defined key + if (localStorage.getItem('sortUsersBy')) { + this.sortBy = localStorage.getItem('sortUsersBy'); + } else { + localStorage.setItem('sortUsersBy', this.sortBy); } - - /** - * returns true, when the user is system admin - * - * @param permissions PermissionData from user profile - */ - userIsSystemAdmin(permissions: Permissions): boolean { - let admin = false; - const groupsPerProjectKeys: string[] = Object.keys( - permissions.groupsPerProject - ); - - for (const key of groupsPerProjectKeys) { - if (key === Constants.SystemProjectIRI) { - admin = - permissions.groupsPerProject[key].indexOf( - Constants.SystemAdminGroupIRI - ) > -1; - } - } - - return admin; + } + + trackByFn = (index: number, item: ReadUser) => `${index}-${item.id}`; + + /** + * returns true, when the user is project admin; + * when the parameter permissions is not set, + * it returns the value for the logged-in user + * + * + * @param [permissions] user's permissions + * @returns boolean + */ + userIsProjectAdmin(permissions?: Permissions): boolean { + if (!this.project) { + return false; } - /** - * update user's group memebership - */ - updateGroupsMembership(id: string, groups: string[]): void { - const currentUserGroups: string[] = []; - this._dspApiConnection.admin.usersEndpoint - .getUserGroupMemberships(id) - .subscribe( - (response: ApiResponseData) => { - for (const group of response.body.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._dspApiConnection.admin.usersEndpoint - .removeUserFromGroupMembership(id, oldGroup) - .pipe( - take(1), - map((response: ApiResponseData) => response.body.user)) - .subscribe((user: ReadUser) => { - if (this.projectUuid) { - this._store.dispatch(new LoadProjectAction(this.projectUuid)); - } - }, - (ngError: ApiResponseError) => { - this._errorHandler.showMessage(ngError); - }); - } - } - for (const newGroup of groups) { - if (currentUserGroups.indexOf(newGroup) === -1) { - this.addUserToGroupMembership(id, newGroup); - } - } - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + return ProjectService.IsMemberOfProjectAdminGroup(permissions.groupsPerProject, this.project.id); + } + + /** + * returns true, when the user is system admin + * + * @param permissions PermissionData from user profile + */ + userIsSystemAdmin(permissions: Permissions): boolean { + let admin = false; + const groupsPerProjectKeys: string[] = Object.keys( + permissions.groupsPerProject + ); + + for (const key of groupsPerProjectKeys) { + if (key === Constants.SystemProjectIRI) { + admin = + permissions.groupsPerProject[key].indexOf( + Constants.SystemAdminGroupIRI + ) > -1; + } } - /** - * update user's admin-group membership - */ - updateProjectAdminMembership(id: string, permissions: Permissions): void { - const currentUser = this._store.selectSnapshot(UserSelectors.user); - if (this.userIsProjectAdmin(permissions)) { - // true = user is already project admin --> remove from admin rights - - this._dspApiConnection.admin.usersEndpoint - .removeUserFromProjectAdminMembership(id, this.project.id) - .subscribe( - (response: ApiResponseData) => { - // if this user is not the logged-in user - if (currentUser.username !== response.body.user.username) { - this._store.dispatch(new SetUserAction(response.body.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); - } - ); - } else { - // false: user isn't project admin yet --> add admin rights - this._dspApiConnection.admin.usersEndpoint - .addUserToProjectAdminMembership(id, this.project.id) - .subscribe( - (response: ApiResponseData) => { - if (currentUser.username !== response.body.user.username) { - this._store.dispatch(new SetUserAction(response.body.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((readUser: ReadUser) => { - this.refreshParent.emit(); - }); - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + return admin; + } + + /** + * update user's group memebership + */ + 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); + } + + 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 LoadProjectAction(this.projectUuid)); } - ); + }, + (ngError: ApiResponseError) => { + this._errorHandler.showMessage(ngError); + }); + } + } + for (const newGroup of groups) { + if (currentUserGroups.indexOf(newGroup) === -1) { + this.addUserToGroupMembership(id, newGroup); + } + } + } + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); } - } - - updateSystemAdminMembership(user: ReadUser, systemAdmin: boolean): void { - this._dspApiConnection.admin.usersEndpoint - .updateUserSystemAdminMembership(user.id, systemAdmin) - .pipe(take(1)) - .subscribe((response: ApiResponseData) => { - this._store.dispatch(new SetUserAction(response.body.user)); - if (this._store.selectSnapshot(UserSelectors.username) !== user.username) { + ); + } + + /** + * update user's admin-group membership + */ + updateProjectAdminMembership(id: string, permissions: Permissions): void { + const currentUser = this._store.selectSnapshot(UserSelectors.user); + if (this.userIsProjectAdmin(permissions)) { + // 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(); - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - }); - } - - /** - * open dialog in every case of modification: - * edit user profile data, update user's password, - * remove user from project or toggle project admin membership, - * delete and reactivate user - * - */ - openDialog(mode: string, user?: ReadUser): void { - const dialogConfig: MatDialogConfig = { - width: '560px', - maxHeight: '80vh', - position: { - top: '112px', - }, - data: { user, mode }, - }; - - const dialogRef = this._dialog.open(DialogComponent, dialogConfig); - - dialogRef.afterClosed().subscribe((response) => { - if (response === true) { - switch (mode) { - case 'removeFromProject': - this._store.dispatch(new RemoveUserFromProjectAction(user.id, this.project.id)); - this._actions$.pipe(ofActionSuccessful(LoadProjectMembersAction)) - .pipe(take(1)) - .subscribe(() => { - this.refreshParent.emit(); - }); - break; - case 'deleteUser': - this.deleteUser(user.id); - break; - case 'activateUser': - this.activateUser(user.id); - break; - } + } 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}`]) + ); + } + }); } - this._cd.markForCheck(); - }); - } - - /** - * delete resp. deactivate user - * - * @param id user's IRI - */ - deleteUser(id: string) { - this._dspApiConnection.admin.usersEndpoint.deleteUser(id) - .pipe(take(1)) - .subscribe((response: ApiResponseData) => { - this._store.dispatch(new SetUserAction(response.body.user)); - this.refreshParent.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } else { + // false: user isn't project admin yet --> add admin rights + this._userApiService + .addToProjectMembership(id, this.project.id) + .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); + } ); } + } + + updateSystemAdminMembership(user: ReadUser, systemAdmin: boolean): void { + 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); + }); + } + + /** + * open dialog in every case of modification: + * edit user profile data, update user's password, + * remove user from project or toggle project admin membership, + * delete and reactivate user + * + */ + openDialog(mode: string, user?: ReadUser): void { + const dialogConfig: MatDialogConfig = { + width: '560px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { user, mode } + }; - /** - * reactivate user - * - * @param id user's IRI - */ - activateUser(id: string) { - this._dspApiConnection.admin.usersEndpoint.updateUserStatus(id, true) - .pipe(take(1)) - .subscribe((response: ApiResponseData) => { - this._store.dispatch(new SetUserAction(response.body.user)); - this.refreshParent.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } - - disableMenu(): boolean { - // disable menu in case of: - // project.status = false - if (this.project && this.project.status === false) { - return true; - } else { - return !this._store.selectSnapshot(UserSelectors.isSysAdmin) - && !this._store.selectSnapshot(ProjectsSelectors.isCurrentProjectAdmin); - } - } + const dialogRef = this._dialog.open(DialogComponent, dialogConfig); - sortList(key: any) { - this.sortBy = key; - this.list = this._sortingService.keySortByAlphabetical(this.list, this.sortBy as any); - localStorage.setItem('sortUsersBy', key); - } + dialogRef.afterClosed().subscribe((response) => { + if (response === true) { + switch (mode) { + case 'removeFromProject': + this._store.dispatch(new RemoveUserFromProjectAction(user.id, this.project.id)); + this._actions$.pipe(ofActionSuccessful(LoadProjectMembersAction)) + .pipe(take(1)) + .subscribe(() => { - private addUserToGroupMembership(id: string, newGroup: string): void { - this._dspApiConnection.admin.usersEndpoint - .addUserToGroupMembership(id, newGroup) - .pipe( - take(1), - map((response: ApiResponseData) => response.body.user)) - .subscribe((user: ReadUser) => { - if (this.projectUuid) { - this._store.dispatch(new LoadProjectAction(this.projectUuid)); - } - //this._store.dispatch(new SetUserAction(user)); - }, - (ngError: ApiResponseError) => { - this._errorHandler.showMessage(ngError); - }); + this.refreshParent.emit(); + }); + break; + case 'deleteUser': + this.deleteUser(user.id); + break; + case 'activateUser': + this.activateUser(user.id); + break; + } + } + this._cd.markForCheck(); + }); + } + + /** + * delete resp. deactivate user + * + * @param id user's IRI + */ + deleteUser(id: string) { + 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); + } + ); + } + + /** + * reactivate user + * + * @param id user's IRI + */ + activateUser(id: string) { + 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); + } + ); + } + + disableMenu(): boolean { + // disable menu in case of: + // project.status = false + if (this.project && this.project.status === false) { + return true; + } else { + return !this._store.selectSnapshot(UserSelectors.isSysAdmin) + && !this._store.selectSnapshot(ProjectsSelectors.isCurrentProjectAdmin); } + } + + sortList(key: any) { + this.sortBy = key; + this.list = this._sortingService.keySortByAlphabetical(this.list, this.sortBy as any); + localStorage.setItem('sortUsersBy', key); + } + + private addUserToGroupMembership(id: string, newGroup: string): void { + this._userApiService.addToGroupMembership(id, newGroup) + .pipe( + take(1) + ) + .subscribe(() => { + if (this.projectUuid) { + this._store.dispatch(new LoadProjectAction(this.projectUuid)); + } + }); + } } diff --git a/apps/dsp-app/src/app/user/account/account.component.ts b/apps/dsp-app/src/app/user/account/account.component.ts index 28c2ae022b..130bbc671a 100644 --- a/apps/dsp-app/src/app/user/account/account.component.ts +++ b/apps/dsp-app/src/app/user/account/account.component.ts @@ -1,119 +1,91 @@ -import { - Component, - EventEmitter, - Inject, - Input, - OnInit, - Output, -} from '@angular/core'; -import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { Title } from '@angular/platform-browser'; -import { - ApiResponseError, - KnoraApiConnection, - ReadUser, -} from '@dasch-swiss/dsp-js'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; +import { ReadUser } from '@dasch-swiss/dsp-js'; import { DialogComponent } from '@dsp-app/src/app/main/dialog/dialog.component'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { apiConnectionTokenProvider } from '../../providers/api-connection-token.provider'; import { LoadUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; import { AuthService } from '@dasch-swiss/vre/shared/app-session'; +import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - selector: 'app-account', - templateUrl: './account.component.html', - styleUrls: ['./account.component.scss'], - providers: [apiConnectionTokenProvider] + selector: 'app-account', + templateUrl: './account.component.html', + styleUrls: ['./account.component.scss'], + providers: [apiConnectionTokenProvider] }) export class AccountComponent implements OnInit { - // in case of modification - @Output() refreshParent: EventEmitter = new EventEmitter(); + // in case of modification + @Output() refreshParent: EventEmitter = new EventEmitter(); - @Input() username: string; + @Input() username: string; - @Select(UserSelectors.user) user$: Observable; - @Select(UserSelectors.isLoading) isLoading$: Observable; + @Select(UserSelectors.user) user$: Observable; + @Select(UserSelectors.isLoading) isLoading$: Observable; - userId = null; + userId = null; - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, - private _titleService: Title, - private _store: Store, - private _authService: AuthService, - ) { - // set the page title - this._titleService.setTitle('Your account'); - } + constructor( + private _userApiService: UserApiService, + private _dialog: MatDialog, + private _titleService: Title, + private _store: Store, + private _authService: AuthService + ) { + this._titleService.setTitle('Your account'); + } - ngOnInit() { - this._store.dispatch(new LoadUserAction(this.username)).pipe( - tap((user: ReadUser) => { - this.userId = user.id; - }) - ); - } + ngOnInit() { + this._store.dispatch(new LoadUserAction(this.username)).pipe( + tap((user: ReadUser) => { + this.userId = user.id; + }) + ); + } - openDialog(mode: string, name: string, id?: string): void { - const dialogConfig: MatDialogConfig = { - width: '560px', - maxHeight: '80vh', - position: { - top: '112px', - }, - data: { name: name, mode: mode }, - }; + openDialog(mode: string, name: string, id?: string): void { + this._dialog.open(DialogComponent, { + width: '560px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { name: name, mode: mode } + }) + .afterClosed().subscribe((response) => { + if (response === true) { + // get the mode + switch (mode) { + case 'deleteUser': + this.deleteUser(id); + break; - const dialogRef = this._dialog.open(DialogComponent, dialogConfig); + case 'activateUser': + this.activateUser(id); + break; + } + } else { + // update the view + this.refreshParent.emit(); + } + }); + } - dialogRef.afterClosed().subscribe((response) => { - if (response === true) { - // get the mode - switch (mode) { - case 'deleteUser': - this.deleteUser(id); - break; + deleteUser(id: string) { + this._userApiService.delete(id).subscribe( + () => { + this._authService.logout(); + }); + } - case 'activateUser': - this.activateUser(id); - break; - } - } else { - // update the view - this.refreshParent.emit(); - } + activateUser(id: string) { + this._userApiService.updateStatus(id, true) + .subscribe( + () => { + this.refreshParent.emit(); }); - } - - deleteUser(id: string) { - this._dspApiConnection.admin.usersEndpoint.deleteUser(id).subscribe( - () => { - this._authService.logout(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } - - activateUser(id: string) { - this._dspApiConnection.admin.usersEndpoint - .updateUserStatus(id, true) - .subscribe( - () => { - // console.log('refresh parent after activate', response); - this.refreshParent.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } + } } 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 b9a744632a..7ae3d3c9b3 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 @@ -24,6 +24,7 @@ import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { UserSelectors } from '@dasch-swiss/vre/shared/app-state'; import { CustomRegex } from '@dsp-app/src/app/workspace/resource/values/custom-regex'; import { Store } from '@ngxs/store'; +import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ selector: 'app-password-form', @@ -90,6 +91,7 @@ export class PasswordFormComponent implements OnInit { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, + private _userApiService: UserApiService, private _errorHandler: AppErrorHandler, private _fb: UntypedFormBuilder, private _notification: NotificationService, @@ -264,8 +266,8 @@ export class PasswordFormComponent implements OnInit { ? this.form.controls.requesterPassword.value : this.confirmForm.controls.requesterPassword.value; - this._dspApiConnection.admin.usersEndpoint - .updateUserPassword( + this._userApiService + .updatePassword( this.user.id, requesterPassword, this.form.controls.password.value 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 5e95227c48..96ccfe5010 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 @@ -1,31 +1,16 @@ import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Inject, - Input, - OnChanges, - OnInit, - Output, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnChanges, + OnInit, + Output } from '@angular/core'; -import { - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, - Validators, -} from '@angular/forms'; -import { - ApiResponseData, - ApiResponseError, - Constants, - KnoraApiConnection, - ReadUser, - StringLiteral, - UpdateUserRequest, - User, - UserResponse, -} from '@dasch-swiss/dsp-js'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { ApiResponseError, Constants, ReadUser, StringLiteral, UpdateUserRequest, User } from '@dasch-swiss/dsp-js'; import { AppGlobal } from '@dsp-app/src/app/app-global'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { existingNamesValidator } from '@dsp-app/src/app/main/directive/existing-name/existing-name.directive'; @@ -33,373 +18,381 @@ import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; import { CustomRegex } from '@dsp-app/src/app/workspace/resource/values/custom-regex'; -import { Observable, combineLatest } from 'rxjs'; -import { Actions, Select, Store, ofActionSuccessful } from '@ngxs/store'; -import { AddUserToProjectMembershipAction, CreateUserAction, LoadProjectMembersAction, ProjectsSelectors, SetUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { combineLatest, Observable } from 'rxjs'; +import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store'; +import { + AddUserToProjectMembershipAction, + CreateUserAction, + ProjectsSelectors, + SetUserAction, + UserSelectors +} from '@dasch-swiss/vre/shared/app-state'; import { take } from 'rxjs/operators'; +import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-user-form', - templateUrl: './user-form.component.html', - styleUrls: ['./user-form.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-user-form', + templateUrl: './user-form.component.html', + styleUrls: ['./user-form.component.scss'] }) export class UserFormComponent implements OnInit, OnChanges { - // the user form can be used in several cases: - // a) guest --> register: create new user - // b) system admin or project admin --> add: create new user - // c) system admin or project admin --> edit: edit (not own) user - // d) logged-in user --> edit: edit own user data - // => so, this component has to know who is who and who is doing what; - // the form needs then some permission checks - - /** - * if the form was built to add new user to project, - * we get a project uuid and a name (e-mail or username) - * from the "add-user-autocomplete" input - */ - @Input() projectUuid?: string; - @Input() user?: ReadUser; - @Input() name?: string; - - /** - * send user data to parent component; - * in case of dialog box? - */ - @Output() closeDialog: EventEmitter = new EventEmitter(); - - /** - * status for the progress indicator - */ - loading = false; - loadingData = true; - title: string; - subtitle: string; - - /** - * define, if the user has system administration permission - */ - sysAdminPermission = false; - - /** - * username should be unique - */ - existingUsernames: [RegExp] = [ - new RegExp('anEmptyRegularExpressionWasntPossible'), - ]; - usernameMinLength = 4; - - /** - * email should be unique - */ - existingEmails: [RegExp] = [ - new RegExp('anEmptyRegularExpressionWasntPossible'), - ]; - - /** - * form group for the form controller - */ - userForm: UntypedFormGroup; - - /** - * error checking on the following fields - */ - formErrors = { - givenName: '', - familyName: '', - email: '', - username: '', - }; - - /** - * error hints - */ - validationMessages = { - givenName: { - required: 'First name is required.', - }, - familyName: { - required: 'Last name is required.', - }, - email: { - required: 'Email address is required.', - pattern: "This doesn't appear to be a valid email address.", - existingName: - 'This user exists already. If you want to edit it, ask a system administrator.', - member: 'This user is already a member of the project.', - }, - username: { - required: 'Username is required.', - pattern: - 'Spaces and special characters are not allowed in username', - minlength: - 'Username must be at least ' + - this.usernameMinLength + - ' characters long.', - existingName: - 'This user exists already. If you want to edit it, ask a system administrator.', - member: 'This user is already a member of the project.', - }, - }; - - /** - * success of sending data - */ - success = false; - /** - * message after successful post - */ - successMessage: any = { - status: 200, - statusText: "You have successfully updated user's profile data.", - }; - - /** - * selector to set default language - */ - languagesList: StringLiteral[] = AppGlobal.languagesList; - - @Select(UserSelectors.allUsers) allUsers$: Observable; - @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; - @Select(ProjectsSelectors.hasLoadingErrors) hasLoadingErrors$: Observable; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _formBuilder: UntypedFormBuilder, - private _notification: NotificationService, - private _projectService: ProjectService, - private _store: Store, - private _actions$: Actions, - private _cd: ChangeDetectorRef, - ) { + // the user form can be used in several cases: + // a) guest --> register: create new user + // b) system admin or project admin --> add: create new user + // c) system admin or project admin --> edit: edit (not own) user + // d) logged-in user --> edit: edit own user data + // => so, this component has to know who is who and who is doing what; + // the form needs then some permission checks + + /** + * if the form was built to add new user to project, + * we get a project uuid and a name (e-mail or username) + * from the "add-user-autocomplete" input + */ + @Input() projectUuid?: string; + @Input() user?: ReadUser; + @Input() name?: string; + + /** + * send user data to parent component; + * in case of dialog box? + */ + @Output() closeDialog: EventEmitter = new EventEmitter(); + + /** + * status for the progress indicator + */ + loading = false; + loadingData = true; + title: string; + subtitle: string; + + /** + * define, if the user has system administration permission + */ + sysAdminPermission = false; + + /** + * username should be unique + */ + existingUsernames: [RegExp] = [ + new RegExp('anEmptyRegularExpressionWasntPossible') + ]; + usernameMinLength = 4; + + /** + * email should be unique + */ + existingEmails: [RegExp] = [ + new RegExp('anEmptyRegularExpressionWasntPossible') + ]; + + /** + * form group for the form controller + */ + userForm: UntypedFormGroup; + + /** + * error checking on the following fields + */ + formErrors = { + givenName: '', + familyName: '', + email: '', + username: '' + }; + + /** + * error hints + */ + validationMessages = { + givenName: { + required: 'First name is required.' + }, + familyName: { + required: 'Last name is required.' + }, + email: { + required: 'Email address is required.', + pattern: 'This doesn\'t appear to be a valid email address.', + existingName: + 'This user exists already. If you want to edit it, ask a system administrator.', + member: 'This user is already a member of the project.' + }, + username: { + required: 'Username is required.', + pattern: + 'Spaces and special characters are not allowed in username', + minlength: + 'Username must be at least ' + + this.usernameMinLength + + ' characters long.', + existingName: + 'This user exists already. If you want to edit it, ask a system administrator.', + member: 'This user is already a member of the project.' } - - ngOnInit() { - this.loadingData = true; - - if (this.user) { - this.title = this.user.username; - this.subtitle = "'appLabels.form.user.title.edit' | translate"; - this.loadingData = !this.buildForm(this.user); - } else { - /** - * create mode: empty form for new user - */ - - // get existing users to avoid same usernames and email addresses - this.allUsers$ - .pipe(take(1)) - .subscribe((allUsers) => { - for (const user of allUsers) { - // email address of the user should be unique. - // therefore we create a list of existing email addresses to avoid multiple use of user names - this.existingEmails.push( - new RegExp('(?:^|W)' + user.email.toLowerCase() + '(?:$|W)')); - // username should also be unique. - // therefore we create a list of existingUsernames to avoid multiple use of user names - this.existingUsernames.push( - new RegExp('(?:^|W)' + user.username.toLowerCase() + '(?:$|W)') - ); - } - - const newUser: ReadUser = new ReadUser(); - - if (CustomRegex.EMAIL_REGEX.test(this.name)) { - newUser.email = this.name; - } else { - newUser.username = this.name; - } - // build the form - this.loadingData = !this.buildForm(newUser); - this._cd.markForCheck(); - }); - } + }; + + /** + * success of sending data + */ + success = false; + /** + * message after successful post + */ + successMessage: any = { + status: 200, + statusText: 'You have successfully updated user\'s profile data.' + }; + + /** + * selector to set default language + */ + languagesList: StringLiteral[] = AppGlobal.languagesList; + + @Select(UserSelectors.allUsers) allUsers$: Observable; + @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; + @Select(ProjectsSelectors.hasLoadingErrors) hasLoadingErrors$: Observable; + + constructor( + @Inject(DspApiConnectionToken) + private _userApiService: UserApiService, + private _errorHandler: AppErrorHandler, + private _formBuilder: UntypedFormBuilder, + private _notification: NotificationService, + private _projectService: ProjectService, + private _store: Store, + private _actions$: Actions, + private _cd: ChangeDetectorRef + ) { + } + + ngOnInit() { + this.loadingData = true; + + if (this.user) { + this.title = this.user.username; + this.subtitle = '\'appLabels.form.user.title.edit\' | translate'; + this.loadingData = !this.buildForm(this.user); + } else { + /** + * create mode: empty form for new user + */ + + // get existing users to avoid same usernames and email addresses + this.allUsers$ + .pipe(take(1)) + .subscribe((allUsers) => { + for (const user of allUsers) { + // email address of the user should be unique. + // therefore we create a list of existing email addresses to avoid multiple use of user names + this.existingEmails.push( + new RegExp('(?:^|W)' + user.email.toLowerCase() + '(?:$|W)')); + // username should also be unique. + // therefore we create a list of existingUsernames to avoid multiple use of user names + this.existingUsernames.push( + new RegExp('(?:^|W)' + user.username.toLowerCase() + '(?:$|W)') + ); + } + + const newUser: ReadUser = new ReadUser(); + + if (CustomRegex.EMAIL_REGEX.test(this.name)) { + newUser.email = this.name; + } else { + newUser.username = this.name; + } + // build the form + this.loadingData = !this.buildForm(newUser); + this._cd.markForCheck(); + }); } + } - ngOnChanges() { - if (this.user) { - this.buildForm(this.user); - } + ngOnChanges() { + if (this.user) { + this.buildForm(this.user); + } + } + + /** + * build the whole form + * + */ + buildForm(user: ReadUser): boolean { + // get info about system admin permission + if ( + user.id && + user.permissions.groupsPerProject[Constants.SystemProjectIRI] + ) { + // this user is member of the system project. does he has admin rights? + this.sysAdminPermission = user.permissions.groupsPerProject[ + Constants.SystemProjectIRI + ].includes(Constants.SystemAdminGroupIRI); } - /** - * build the whole form - * - */ - buildForm(user: ReadUser): boolean { - // get info about system admin permission - if ( - user.id && - user.permissions.groupsPerProject[Constants.SystemProjectIRI] - ) { - // this user is member of the system project. does he has admin rights? - this.sysAdminPermission = user.permissions.groupsPerProject[ - Constants.SystemProjectIRI - ].includes(Constants.SystemAdminGroupIRI); - } - - // if user is defined, we're in the edit mode - // otherwise "create new user" mode is active - const editMode = !!user.id; - - this.userForm = this._formBuilder.group({ - givenName: new UntypedFormControl( - { - value: user.givenName, - disabled: false, - }, - [Validators.required] - ), - familyName: new UntypedFormControl( - { - value: user.familyName, - disabled: false, - }, - [Validators.required] - ), - email: new UntypedFormControl( - { - value: user.email, - disabled: editMode, - }, - [ - Validators.required, - Validators.pattern(CustomRegex.EMAIL_REGEX), - existingNamesValidator(this.existingEmails), - ] - ), - username: new UntypedFormControl( - { - value: user.username, - disabled: editMode, - }, - [ - Validators.required, - Validators.minLength(4), - Validators.pattern(CustomRegex.USERNAME_REGEX), - existingNamesValidator(this.existingUsernames), - ] - ), - password: new UntypedFormControl({ - value: '', - disabled: editMode, - }), - lang: new UntypedFormControl({ - value: user.lang ? user.lang : 'en', - disabled: false, - }), - status: new UntypedFormControl({ - value: user.status ? user.status : true, - disabled: editMode, - }), - systemAdmin: new UntypedFormControl({ - value: this.sysAdminPermission, - disabled: editMode, - }), - // 'systemAdmin': this.sysAdminPermission, - // 'group': null - }); + // if user is defined, we're in the edit mode + // otherwise "create new user" mode is active + const editMode = !!user.id; - this.userForm.valueChanges.subscribe(() => this.onValueChanged()); - return true; + this.userForm = this._formBuilder.group({ + givenName: new UntypedFormControl( + { + value: user.givenName, + disabled: false + }, + [Validators.required] + ), + familyName: new UntypedFormControl( + { + value: user.familyName, + disabled: false + }, + [Validators.required] + ), + email: new UntypedFormControl( + { + value: user.email, + disabled: editMode + }, + [ + Validators.required, + Validators.pattern(CustomRegex.EMAIL_REGEX), + existingNamesValidator(this.existingEmails) + ] + ), + username: new UntypedFormControl( + { + value: user.username, + disabled: editMode + }, + [ + Validators.required, + Validators.minLength(4), + Validators.pattern(CustomRegex.USERNAME_REGEX), + existingNamesValidator(this.existingUsernames) + ] + ), + password: new UntypedFormControl({ + value: '', + disabled: editMode + }), + lang: new UntypedFormControl({ + value: user.lang ? user.lang : 'en', + disabled: false + }), + status: new UntypedFormControl({ + value: user.status ? user.status : true, + disabled: editMode + }), + systemAdmin: new UntypedFormControl({ + value: this.sysAdminPermission, + disabled: editMode + }) + // 'systemAdmin': this.sysAdminPermission, + // 'group': null + }); + + this.userForm.valueChanges.subscribe(() => this.onValueChanged()); + return true; + } + + onValueChanged() { + if (!this.userForm) { + return; } - onValueChanged() { - if (!this.userForm) { - return; - } + const form = this.userForm; - const form = this.userForm; - - Object.keys(this.formErrors).map((field) => { - this.formErrors[field] = ''; - const control = form.get(field); - if (control && control.dirty && !control.valid) { - const messages = this.validationMessages[field]; - Object.keys(control.errors).map((key) => { - this.formErrors[field] += messages[key] + ' '; - }); - } + Object.keys(this.formErrors).map((field) => { + this.formErrors[field] = ''; + const control = form.get(field); + if (control && control.dirty && !control.valid) { + const messages = this.validationMessages[field]; + Object.keys(control.errors).map((key) => { + this.formErrors[field] += messages[key] + ' '; }); - } + } + }); + } + + // get password from password form and send it to user form + getPassword(pw: string) { + this.userForm.controls.password.setValue(pw); + } + + submitData(): void { + this.loading = true; + + if (this.user) { + // edit mode: update user data + // username doesn't seem to be optional in @dasch-swiss/dsp-js usersEndpoint type UpdateUserRequest. + // but a user can't change the username, the field is disabled, so it's not a value in this form. + // we have to make a small hack here. + const userData: UpdateUserRequest = new UpdateUserRequest(); + // userData.username = this.userForm.value.username; + userData.familyName = this.userForm.value.familyName; + userData.givenName = this.userForm.value.givenName; + // userData.email = this.userForm.value.email; + userData.lang = this.userForm.value.lang; + + this._userApiService + .updateBasicInformation(this.user.id, userData) + .subscribe( + response => { + this.user = response.user; + this.buildForm(this.user); + const user = this._store.selectSnapshot(UserSelectors.user) as ReadUser; + // update application state + if (user.username === this.user.username) { + // update logged in user session + this.user.lang = this.userForm.controls['lang'].value; + } - // get password from password form and send it to user form - getPassword(pw: string) { - this.userForm.controls.password.setValue(pw); + this._store.dispatch(new SetUserAction(this.user)); + 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); } + } + + private createNewUser(userForm: any): void { + const userData: User = new User(); + userData.username = userForm.username; + userData.familyName = userForm.familyName; + userData.givenName = userForm.givenName; + userData.email = userForm.email; + userData.password = userForm.password; + userData.systemAdmin = userForm.systemAdmin; + userData.status = userForm.status; + userData.lang = userForm.lang; + + this._store.dispatch(new CreateUserAction(userData)); + combineLatest([this._actions$.pipe(ofActionSuccessful(CreateUserAction)), this.allUsers$]) + .pipe(take(1)) + .subscribe(([loadUsersAction, allUsers]) => { + this.user = allUsers.find(user => user.username === loadUsersAction.userData.username); + this.buildForm(this.user); + if (this.projectUuid) { + // if a projectUuid exists, add the user to the project + const projectIri = this._projectService.uuidToIri(this.projectUuid); + this._store.dispatch(new AddUserToProjectMembershipAction(this.user.id, projectIri)); - submitData(): void { - this.loading = true; - - if (this.user) { - // edit mode: update user data - // username doesn't seem to be optional in @dasch-swiss/dsp-js usersEndpoint type UpdateUserRequest. - // but a user can't change the username, the field is disabled, so it's not a value in this form. - // we have to make a small hack here. - const userData: UpdateUserRequest = new UpdateUserRequest(); - // userData.username = this.userForm.value.username; - userData.familyName = this.userForm.value.familyName; - userData.givenName = this.userForm.value.givenName; - // userData.email = this.userForm.value.email; - userData.lang = this.userForm.value.lang; - - this._dspApiConnection.admin.usersEndpoint - .updateUserBasicInformation(this.user.id, userData) - .subscribe( - (response: ApiResponseData) => { - this.user = response.body.user; - this.buildForm(this.user); - const user = this._store.selectSnapshot(UserSelectors.user) as ReadUser; - // update application state - if (user.username === this.user.username) { - // update logged in user session - this.user.lang = this.userForm.controls['lang'].value; - } - - this._store.dispatch(new SetUserAction(this.user)); - 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); } - } - private createNewUser(userForm: any): void { - const userData: User = new User(); - userData.username = userForm.username; - userData.familyName = userForm.familyName; - userData.givenName = userForm.givenName; - userData.email = userForm.email; - userData.password = userForm.password; - userData.systemAdmin = userForm.systemAdmin; - userData.status = userForm.status; - userData.lang = userForm.lang; - - this._store.dispatch(new CreateUserAction(userData)); - combineLatest([this._actions$.pipe(ofActionSuccessful(CreateUserAction)), this.allUsers$]) - .pipe(take(1)) - .subscribe(([loadUsersAction, allUsers]) => { - this.user = allUsers.find(user => user.username === loadUsersAction.userData.username); - this.buildForm(this.user); - if (this.projectUuid) { - // if a projectUuid exists, add the user to the project - const projectIri = this._projectService.uuidToIri(this.projectUuid); - this._store.dispatch(new AddUserToProjectMembershipAction(this.user.id, projectIri)); - } - - this.closeDialog.emit(this.user); - this.loading = false; - }); - } + this.closeDialog.emit(this.user); + this.loading = false; + }); + } } 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 15c12fbcda..a249f3230d 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,203 +1,196 @@ -import { - Component, - EventEmitter, - Inject, - Input, - OnInit, - Output, - ViewChild, -} from '@angular/core'; +import { Component, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { ApiResponseData, ProjectResponse } from '@dasch-swiss/dsp-js'; import { - ApiResponseError, - Constants, - CreateFileValue, - CreateResource, - CreateTextValueAsString, - CreateValue, - KnoraApiConnection, - ReadResource, - ResourceClassAndPropertyDefinitions, - ResourceClassDefinition, - ResourcePropertyDefinition, + ApiResponseError, + Constants, + CreateFileValue, + CreateResource, + CreateTextValueAsString, + CreateValue, + KnoraApiConnection, + ReadResource, + ResourceClassAndPropertyDefinitions, + ResourceClassDefinition, + ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { DialogEvent } from '@dsp-app/src/app/main/dialog/dialog.component'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { SelectPropertiesComponent } from '../../resource-instance-form/select-properties/select-properties.component'; +import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; @Component({ - selector: 'app-create-link-resource', - templateUrl: './create-link-resource.component.html', - styleUrls: ['./create-link-resource.component.scss'], + selector: 'app-create-link-resource', + templateUrl: './create-link-resource.component.html', + styleUrls: ['./create-link-resource.component.scss'] }) export class CreateLinkResourceComponent implements OnInit { - @Input() parentResource: ReadResource; - @Input() propDef: string; - @Input() resourceClassDef: string; - @Input() currentOntoIri: string; - - @Output() closeDialog: EventEmitter = - new EventEmitter(); - - @ViewChild('selectProps') - selectPropertiesComponent: SelectPropertiesComponent; - - properties: ResourcePropertyDefinition[]; - propertiesForm: UntypedFormGroup; - resourceClass: ResourceClassDefinition; - ontologyInfo: ResourceClassAndPropertyDefinitions; - fileValue: CreateFileValue; - - hasFileValue: - | 'stillImage' - | 'movingImage' - | 'audio' - | 'document' - | 'text' - | 'archive'; - - propertiesObj = {}; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _fb: UntypedFormBuilder, - private _errorHandler: AppErrorHandler - ) {} - - ngOnInit(): void { - this.propertiesForm = this._fb.group({}); - - this._dspApiConnection.v2.ontologyCache - .getResourceClassDefinition(this.resourceClassDef) - .subscribe((onto: ResourceClassAndPropertyDefinitions) => { - this.ontologyInfo = onto; - this.resourceClass = onto.classes[this.resourceClassDef]; - this.properties = onto - .getPropertyDefinitionsByType(ResourcePropertyDefinition) - .filter( - (prop) => - !prop.isLinkProperty && - prop.isEditable && - prop.id !== Constants.HasStillImageFileValue && - prop.id !== Constants.HasDocumentFileValue && - prop.id !== Constants.HasAudioFileValue && - prop.id !== Constants.HasArchiveFileValue && - prop.id !== Constants.HasMovingImageFileValue && - prop.id !== Constants.HasTextFileValue - ); - - if (onto.properties[Constants.HasStillImageFileValue]) { - this.hasFileValue = 'stillImage'; - } else if (onto.properties[Constants.HasDocumentFileValue]) { - this.hasFileValue = 'document'; - } else if (onto.properties[Constants.HasAudioFileValue]) { - this.hasFileValue = 'audio'; - } else if (onto.properties[Constants.HasArchiveFileValue]) { - this.hasFileValue = 'archive'; - } else if (onto.properties[Constants.HasMovingImageFileValue]) { - this.hasFileValue = 'movingImage'; - } else if (onto.properties[Constants.HasTextFileValue]) { - this.hasFileValue = 'text'; - } else { - this.hasFileValue = undefined; - } - }); - } - - onSubmit() { - if (this.propertiesForm.valid) { - this._dspApiConnection.admin.projectsEndpoint - .getProjectByShortcode(this.resourceClassDef.split('/')[4]) - .subscribe((project: ApiResponseData) => { - const createResource = new CreateResource(); - - const resLabelVal = ( - this.selectPropertiesComponent.createValueComponent.getNewValue() - ); - - createResource.label = resLabelVal.text; - - createResource.type = this.resourceClassDef; - - createResource.attachedToProject = project.body.project.id; - - this.selectPropertiesComponent.switchPropertiesComponent.forEach( - (child) => { - const createVal = - child.createValueComponent.getNewValue(); - const iri = child.property.id; - if (createVal instanceof CreateValue) { - if (this.propertiesObj[iri]) { - // if a key already exists, add the createVal to the array - this.propertiesObj[iri].push(createVal); - } else { - // if no key exists, add one and add the createVal as the first value of the array - this.propertiesObj[iri] = [createVal]; - } - } - } - ); - - if (this.fileValue) { - switch (this.hasFileValue) { - case 'stillImage': - this.propertiesObj[ - Constants.HasStillImageFileValue - ] = [this.fileValue]; - break; - case 'document': - this.propertiesObj[ - Constants.HasDocumentFileValue - ] = [this.fileValue]; - break; - case 'audio': - this.propertiesObj[ - Constants.HasAudioFileValue - ] = [this.fileValue]; - break; - case 'movingImage': - this.propertiesObj[ - Constants.HasMovingImageFileValue - ] = [this.fileValue]; - break; - case 'archive': - this.propertiesObj[ - Constants.HasArchiveFileValue - ] = [this.fileValue]; - break; - case 'text': - this.propertiesObj[Constants.HasTextFileValue] = - [this.fileValue]; - } - } - - createResource.properties = this.propertiesObj; - - this._dspApiConnection.v2.res - .createResource(createResource) - .subscribe( - (res: ReadResource) => { - this.closeDialog.emit(res); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - }); + @Input() parentResource: ReadResource; + @Input() propDef: string; + @Input() resourceClassDef: string; + @Input() currentOntoIri: string; + + @Output() closeDialog: EventEmitter = + new EventEmitter(); + + @ViewChild('selectProps') + selectPropertiesComponent: SelectPropertiesComponent; + + properties: ResourcePropertyDefinition[]; + propertiesForm: UntypedFormGroup; + resourceClass: ResourceClassDefinition; + ontologyInfo: ResourceClassAndPropertyDefinitions; + fileValue: CreateFileValue; + + hasFileValue: + | 'stillImage' + | 'movingImage' + | 'audio' + | 'document' + | 'text' + | 'archive'; + + propertiesObj = {}; + + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _projectApiService: ProjectApiService, + private _fb: UntypedFormBuilder, + private _errorHandler: AppErrorHandler + ) { + } + + ngOnInit(): void { + this.propertiesForm = this._fb.group({}); + + this._dspApiConnection.v2.ontologyCache + .getResourceClassDefinition(this.resourceClassDef) + .subscribe((onto: ResourceClassAndPropertyDefinitions) => { + this.ontologyInfo = onto; + this.resourceClass = onto.classes[this.resourceClassDef]; + this.properties = onto + .getPropertyDefinitionsByType(ResourcePropertyDefinition) + .filter( + (prop) => + !prop.isLinkProperty && + prop.isEditable && + prop.id !== Constants.HasStillImageFileValue && + prop.id !== Constants.HasDocumentFileValue && + prop.id !== Constants.HasAudioFileValue && + prop.id !== Constants.HasArchiveFileValue && + prop.id !== Constants.HasMovingImageFileValue && + prop.id !== Constants.HasTextFileValue + ); + + if (onto.properties[Constants.HasStillImageFileValue]) { + this.hasFileValue = 'stillImage'; + } else if (onto.properties[Constants.HasDocumentFileValue]) { + this.hasFileValue = 'document'; + } else if (onto.properties[Constants.HasAudioFileValue]) { + this.hasFileValue = 'audio'; + } else if (onto.properties[Constants.HasArchiveFileValue]) { + this.hasFileValue = 'archive'; + } else if (onto.properties[Constants.HasMovingImageFileValue]) { + this.hasFileValue = 'movingImage'; + } else if (onto.properties[Constants.HasTextFileValue]) { + this.hasFileValue = 'text'; } else { - this.propertiesForm.markAllAsTouched(); + this.hasFileValue = undefined; } + }); + } + + onSubmit() { + if (this.propertiesForm.valid) { + this._projectApiService.get(this.resourceClassDef.split('/')[4], 'shortcode') + .subscribe(response => { + const createResource = new CreateResource(); + + const resLabelVal = ( + this.selectPropertiesComponent.createValueComponent.getNewValue() + ); + + createResource.label = resLabelVal.text; + + createResource.type = this.resourceClassDef; + + createResource.attachedToProject = response.project.id; + + this.selectPropertiesComponent.switchPropertiesComponent.forEach( + (child) => { + const createVal = + child.createValueComponent.getNewValue(); + const iri = child.property.id; + if (createVal instanceof CreateValue) { + if (this.propertiesObj[iri]) { + // if a key already exists, add the createVal to the array + this.propertiesObj[iri].push(createVal); + } else { + // if no key exists, add one and add the createVal as the first value of the array + this.propertiesObj[iri] = [createVal]; + } + } + } + ); + + if (this.fileValue) { + switch (this.hasFileValue) { + case 'stillImage': + this.propertiesObj[ + Constants.HasStillImageFileValue + ] = [this.fileValue]; + break; + case 'document': + this.propertiesObj[ + Constants.HasDocumentFileValue + ] = [this.fileValue]; + break; + case 'audio': + this.propertiesObj[ + Constants.HasAudioFileValue + ] = [this.fileValue]; + break; + case 'movingImage': + this.propertiesObj[ + Constants.HasMovingImageFileValue + ] = [this.fileValue]; + break; + case 'archive': + this.propertiesObj[ + Constants.HasArchiveFileValue + ] = [this.fileValue]; + break; + case 'text': + this.propertiesObj[Constants.HasTextFileValue] = + [this.fileValue]; + } + } + + createResource.properties = this.propertiesObj; + + this._dspApiConnection.v2.res + .createResource(createResource) + .subscribe( + (res: ReadResource) => { + this.closeDialog.emit(res); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + }); + } else { + this.propertiesForm.markAllAsTouched(); } + } - onCancel() { - // emit DialogCanceled event - this.closeDialog.emit(DialogEvent.DialogCanceled); - } + onCancel() { + // emit DialogCanceled event + this.closeDialog.emit(DialogEvent.DialogCanceled); + } - setFileValue(file: CreateFileValue) { - this.fileValue = file; - } + setFileValue(file: CreateFileValue) { + this.fileValue = file; + } } 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 77944ad9f4..25a63becc7 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 @@ -13,7 +13,6 @@ import { import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { PageEvent } from '@angular/material/paginator'; import { - ApiResponseData, ApiResponseError, CardinalityUtil, Constants, @@ -24,7 +23,6 @@ import { IHasPropertyWithPropertyDefinition, KnoraApiConnection, PermissionUtil, - ProjectResponse, PropertyDefinition, ReadLinkValue, ReadProject, @@ -36,25 +34,22 @@ import { ResourcePropertyDefinition, UpdateResourceMetadata, UpdateResourceMetadataResponse, - UserResponse, + UserResponse } from '@dasch-swiss/dsp-js'; -import {Observable, Subscription, forkJoin} from 'rxjs'; +import { forkJoin, Observable, Subscription } from 'rxjs'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { - ConfirmationWithComment, - DialogComponent, -} from '@dsp-app/src/app/main/dialog/dialog.component'; +import { ConfirmationWithComment, DialogComponent } from '@dsp-app/src/app/main/dialog/dialog.component'; import { ComponentCommunicationEventService, EmitEvent, - Events as CommsEvents, + Events as CommsEvents } from '@dsp-app/src/app/main/services/component-communication-event.service'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { DspResource } from '../dsp-resource'; import { RepresentationConstants } from '../representation/file-representation'; import { IncomingService } from '../services/incoming.service'; -import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; +import { ProjectService, SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; import { ResourceService } from '../services/resource.service'; import { UserService } from '../services/user.service'; import { @@ -62,678 +57,674 @@ import { DeletedEventValue, Events, UpdatedEventValues, - ValueOperationEventService, + ValueOperationEventService } from '../services/value-operation-event.service'; import { ValueService } from '../services/value.service'; -import { SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; +import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; // object of property information from ontology class, properties and property values export interface PropertyInfoValues { - guiDef: IHasPropertyWithPropertyDefinition; - propDef: PropertyDefinition; - values: ReadValue[]; + guiDef: IHasPropertyWithPropertyDefinition; + propDef: PropertyDefinition; + values: ReadValue[]; } @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-properties', - templateUrl: './properties.component.html', - styleUrls: ['./properties.component.scss'] + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-properties', + templateUrl: './properties.component.html', + styleUrls: ['./properties.component.scss'] }) export class PropertiesComponent implements OnInit, OnChanges, OnDestroy { - /** - * input `resource` of properties component: - * complete information about the current resource - */ - @Input() resource: DspResource; - - /** - * input `displayProjectInfo` of properties component: - * display project info or not; "This resource belongs to project XYZ" - */ - @Input() displayProjectInfo = false; - - /** - * does the logged-in user has system or project admin permissions? - */ - @Input() adminPermissions = false; - - /** - * in case properties belongs to an annotation (e.g. region in still images) - * in this case we don't have to display the isRegionOf property - */ - @Input() isAnnotation = false; - - @Input() valueUuidToHighlight: string; - - /** - * output `referredProjectClicked` of resource view component: - * can be used to go to project page - */ - @Output() referredProjectClicked: EventEmitter = - new EventEmitter(); - - /** - * output `referredProjectHovered` of resource view component: - * can be used for preview when hovering on project - */ - @Output() referredProjectHovered: EventEmitter = - new EventEmitter(); - - /** - * output `referredResourceClicked` of resource view component: - * can be used to open resource - */ - @Output() referredResourceClicked: EventEmitter = - new EventEmitter(); - - /** - * output `referredResourceHovered` of resource view component: - * can be used for preview when hovering on resource - */ - @Output() referredResourceHovered: EventEmitter = - new EventEmitter(); - - @Output() regionChanged: EventEmitter = - new EventEmitter(); - - @Output() regionDeleted: EventEmitter = - new EventEmitter(); - - readonly amount_resources = 25; - - lastModificationDate: string; - - deletedResource = false; - - userCanDelete: boolean; - cantDeleteReason = ''; - - userCanEdit: boolean; - addValueFormIsVisible: boolean; // used to toggle add value form field - propID: string; // used in template to show only the add value form of the corresponding value - - valueOperationEventSubscriptions: Subscription[] = []; // array of ValueOperationEvent subscriptions - - representationConstants = RepresentationConstants; - - numberOffAllIncomingLinkRes: number; - allIncomingLinkResources: ReadResource[] = []; - displayedIncomingLinkResources: ReadResource[] = []; - hasIncomingLinkIri = Constants.HasIncomingLinkValue; - - project: ReadProject; - user: ReadUser; - - pageEvent: PageEvent; - loading = false; - - showAllProps = false; // show or hide empty properties - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _dialog: MatDialog, - private _errorHandler: AppErrorHandler, - private _incomingService: IncomingService, - private _notification: NotificationService, - private _resourceService: ResourceService, - private _userService: UserService, - private _valueOperationEventService: ValueOperationEventService, - private _valueService: ValueService, - private _componentCommsService: ComponentCommunicationEventService, - private _projectService: ProjectService, - private _sortingService: SortingService, - private _cd: ChangeDetectorRef, - ) {} - - ngOnInit(): void { - // reset the page event - this.pageEvent = new PageEvent(); - this.pageEvent.pageIndex = 0; - - this._getAllIncomingLinkRes(); - - if (this.resource.res) { - // get user permissions - const allPermissions = PermissionUtil.allUserPermissions( - this.resource.res.userHasPermission as - | 'RV' - | 'V' - | 'M' - | 'D' - | 'CR' - ); - - // get last modification date - this.lastModificationDate = this.resource.res.lastModificationDate; - - // if user has modify permissions, set addButtonIsVisible to true so the user see's the add button - this.userCanEdit = allPermissions.indexOf(PermissionUtil.Permissions.M) !== -1; - - // if user has delete permissions - this.userCanDelete = - allPermissions.indexOf(PermissionUtil.Permissions.D) !== -1; - } - - // listen for the AddValue event to be emitted and call hideAddValueForm() - // this._valueOperationEventService.on(Events.ValueAdded, () => this.hideAddValueForm()) - this.valueOperationEventSubscriptions = []; - - // subscribe to the ValueOperationEventService and listen for an event to be emitted - this.valueOperationEventSubscriptions.push( - this._valueOperationEventService.on( - Events.ValueAdded, - (newValue: AddedEventValue) => { - if (newValue) { - this.lastModificationDate = - newValue.addedValue.valueCreationDate; - this.addValueToResource(newValue.addedValue); - this.hideAddValueForm(); - } - } - ) - ); - - this.valueOperationEventSubscriptions.push( - this._valueOperationEventService.on( - Events.ValueUpdated, - (updatedValue: UpdatedEventValues) => { - this.lastModificationDate = - updatedValue.updatedValue.valueCreationDate; - this.updateValueInResource( - updatedValue.currentValue, - updatedValue.updatedValue - ); - this.hideAddValueForm(); - } - ) - ); - - this.valueOperationEventSubscriptions.push( - this._valueOperationEventService.on( - Events.ValueDeleted, - (deletedValue: DeletedEventValue) => { - // the DeletedEventValue does not contain a creation or last modification date - // so, we have to grab it from res info - this._getLastModificationDate(this.resource.res.id); - this.deleteValueFromResource(deletedValue.deletedValue); - } - ) - ); - - // keep the information if the user wants to display all properties or not - if (localStorage.getItem('showAllProps')) { - this.showAllProps = JSON.parse( - localStorage.getItem('showAllProps') - ); - } else { - localStorage.setItem( - 'showAllProps', - JSON.stringify(this.showAllProps) - ); - } - } - - ngOnChanges(): void { - // get project information - this._dspApiConnection.admin.projectsEndpoint - .getProjectByIri(this.resource.res.attachedToProject) - .subscribe( - (response: ApiResponseData) => { - this.project = response.body.project; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - - // get user information - this._userService - .getUser(this.resource.res.attachedToUser) - .subscribe((response: UserResponse) => { - this.user = response.user; - }); - - this._getAllIncomingLinkRes(); + /** + * input `resource` of properties component: + * complete information about the current resource + */ + @Input() resource: DspResource; + + /** + * input `displayProjectInfo` of properties component: + * display project info or not; "This resource belongs to project XYZ" + */ + @Input() displayProjectInfo = false; + + /** + * does the logged-in user has system or project admin permissions? + */ + @Input() adminPermissions = false; + + /** + * in case properties belongs to an annotation (e.g. region in still images) + * in this case we don't have to display the isRegionOf property + */ + @Input() isAnnotation = false; + + @Input() valueUuidToHighlight: string; + + /** + * output `referredProjectClicked` of resource view component: + * can be used to go to project page + */ + @Output() referredProjectClicked: EventEmitter = + new EventEmitter(); + + /** + * output `referredProjectHovered` of resource view component: + * can be used for preview when hovering on project + */ + @Output() referredProjectHovered: EventEmitter = + new EventEmitter(); + + /** + * output `referredResourceClicked` of resource view component: + * can be used to open resource + */ + @Output() referredResourceClicked: EventEmitter = + new EventEmitter(); + + /** + * output `referredResourceHovered` of resource view component: + * can be used for preview when hovering on resource + */ + @Output() referredResourceHovered: EventEmitter = + new EventEmitter(); + + @Output() regionChanged: EventEmitter = + new EventEmitter(); + + @Output() regionDeleted: EventEmitter = + new EventEmitter(); + + readonly amount_resources = 25; + + lastModificationDate: string; + + deletedResource = false; + + userCanDelete: boolean; + cantDeleteReason = ''; + + userCanEdit: boolean; + addValueFormIsVisible: boolean; // used to toggle add value form field + propID: string; // used in template to show only the add value form of the corresponding value + + valueOperationEventSubscriptions: Subscription[] = []; // array of ValueOperationEvent subscriptions + + representationConstants = RepresentationConstants; + + numberOffAllIncomingLinkRes: number; + allIncomingLinkResources: ReadResource[] = []; + displayedIncomingLinkResources: ReadResource[] = []; + hasIncomingLinkIri = Constants.HasIncomingLinkValue; + + project: ReadProject; + user: ReadUser; + + pageEvent: PageEvent; + loading = false; + + showAllProps = false; // show or hide empty properties + + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _projectApiService: ProjectApiService, + private _dialog: MatDialog, + private _errorHandler: AppErrorHandler, + private _incomingService: IncomingService, + private _notification: NotificationService, + private _resourceService: ResourceService, + private _userService: UserService, + private _valueOperationEventService: ValueOperationEventService, + private _valueService: ValueService, + private _componentCommsService: ComponentCommunicationEventService, + private _projectService: ProjectService, + private _sortingService: SortingService, + private _cd: ChangeDetectorRef + ) { + } + + ngOnInit(): void { + // reset the page event + this.pageEvent = new PageEvent(); + this.pageEvent.pageIndex = 0; + + this._getAllIncomingLinkRes(); + + if (this.resource.res) { + // get user permissions + const allPermissions = PermissionUtil.allUserPermissions( + this.resource.res.userHasPermission as + | 'RV' + | 'V' + | 'M' + | 'D' + | 'CR' + ); + + // get last modification date + this.lastModificationDate = this.resource.res.lastModificationDate; + + // if user has modify permissions, set addButtonIsVisible to true so the user see's the add button + this.userCanEdit = allPermissions.indexOf(PermissionUtil.Permissions.M) !== -1; + + // if user has delete permissions + this.userCanDelete = + allPermissions.indexOf(PermissionUtil.Permissions.D) !== -1; } - ngOnDestroy() { - // unsubscribe from the ValueOperationEventService when component is destroyed - if (this.valueOperationEventSubscriptions !== undefined) { - this.valueOperationEventSubscriptions.forEach((sub) => - sub.unsubscribe() - ); + // listen for the AddValue event to be emitted and call hideAddValueForm() + // this._valueOperationEventService.on(Events.ValueAdded, () => this.hideAddValueForm()) + this.valueOperationEventSubscriptions = []; + + // subscribe to the ValueOperationEventService and listen for an event to be emitted + this.valueOperationEventSubscriptions.push( + this._valueOperationEventService.on( + Events.ValueAdded, + (newValue: AddedEventValue) => { + if (newValue) { + this.lastModificationDate = + newValue.addedValue.valueCreationDate; + this.addValueToResource(newValue.addedValue); + this.hideAddValueForm(); + } + } + ) + ); + + this.valueOperationEventSubscriptions.push( + this._valueOperationEventService.on( + Events.ValueUpdated, + (updatedValue: UpdatedEventValues) => { + this.lastModificationDate = + updatedValue.updatedValue.valueCreationDate; + this.updateValueInResource( + updatedValue.currentValue, + updatedValue.updatedValue + ); + this.hideAddValueForm(); + } + ) + ); + + this.valueOperationEventSubscriptions.push( + this._valueOperationEventService.on( + Events.ValueDeleted, + (deletedValue: DeletedEventValue) => { + // the DeletedEventValue does not contain a creation or last modification date + // so, we have to grab it from res info + this._getLastModificationDate(this.resource.res.id); + this.deleteValueFromResource(deletedValue.deletedValue); } + ) + ); + + // keep the information if the user wants to display all properties or not + if (localStorage.getItem('showAllProps')) { + this.showAllProps = JSON.parse( + localStorage.getItem('showAllProps') + ); + } else { + localStorage.setItem( + 'showAllProps', + JSON.stringify(this.showAllProps) + ); } + } - /** - * opens project - * @param project - */ - openProject(project: ReadProject) { - const uuid = this._projectService.iriToUuid(project.id); - window.open('/project/' + uuid, '_blank'); - } + ngOnChanges(): void { + this._projectApiService.get(this.resource.res.attachedToProject) + .subscribe( + response => { + this.project = response.project; + }); - previewProject() { - // --> TODO: pop up project preview on hover + // get user information + this._userService + .getUser(this.resource.res.attachedToUser) + .subscribe((response: UserResponse) => { + this.user = response.user; + }); + + this._getAllIncomingLinkRes(); + } + + ngOnDestroy() { + // unsubscribe from the ValueOperationEventService when component is destroyed + if (this.valueOperationEventSubscriptions !== undefined) { + this.valueOperationEventSubscriptions.forEach((sub) => + sub.unsubscribe() + ); } - - /** - * goes to the next page of the incoming link pagination - * @param page - */ - goToPage(page: PageEvent) { - this.pageEvent = page; - this._getDisplayedIncomingLinkRes(); + } + + /** + * opens project + * @param project + */ + openProject(project: ReadProject) { + const uuid = this._projectService.iriToUuid(project.id); + window.open('/project/' + uuid, '_blank'); + } + + previewProject() { + // --> TODO: pop up project preview on hover + } + + /** + * goes to the next page of the incoming link pagination + * @param page + */ + goToPage(page: PageEvent) { + this.pageEvent = page; + this._getDisplayedIncomingLinkRes(); + } + + handleIncomingLinkForward() { + if (this.allIncomingLinkResources.length / this.amount_resources > this.pageEvent.pageIndex + 1) { + const newPage = new PageEvent(); + newPage.pageIndex = this.pageEvent.pageIndex + 1; + this.goToPage(newPage); } + } - handleIncomingLinkForward(){ - if(this.allIncomingLinkResources.length / this.amount_resources > this.pageEvent.pageIndex + 1){ - const newPage = new PageEvent(); - newPage.pageIndex = this.pageEvent.pageIndex + 1; - this.goToPage(newPage); - } + handleIncomingLinkBackward() { + if (this.pageEvent.pageIndex > 0) { + const newPage = new PageEvent(); + newPage.pageIndex = this.pageEvent.pageIndex - 1; + this.goToPage(newPage); } - handleIncomingLinkBackward(){ - if (this.pageEvent.pageIndex > 0){ - const newPage = new PageEvent(); - newPage.pageIndex = this.pageEvent.pageIndex - 1; - this.goToPage(newPage); + } + + /** + * opens resource + * @param linkValue + */ + openResource(linkValue: ReadLinkValue | string) { + const iri = + typeof linkValue == 'string' + ? linkValue + : linkValue.linkedResourceIri; + const path = this._resourceService.getResourcePath(iri); + window.open('/resource' + path, '_blank'); + } + + previewResource() { + // --> TODO: pop up resource preview on hover + } + + openDialog(type: 'delete' | 'erase' | 'edit') { + const dialogConfig: MatDialogConfig = { + width: '560px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { mode: type + 'Resource', title: this.resource.res.label } + }; + + const dialogRef = this._dialog.open(DialogComponent, dialogConfig); + + dialogRef.afterClosed().subscribe((answer: ConfirmationWithComment) => { + if (!answer) { + // if the user clicks outside of the dialog window, answer is undefined + return; + } + if (answer.confirmed === true) { + if (type !== 'edit') { + const payload = new DeleteResource(); + payload.id = this.resource.res.id; + payload.type = this.resource.res.type; + payload.deleteComment = answer.comment + ? answer.comment + : undefined; + payload.lastModificationDate = this.lastModificationDate; + switch (type) { + case 'delete': + // delete the resource and refresh the view + this._dspApiConnection.v2.res + .deleteResource(payload) + .subscribe( + (response: DeleteResourceResponse) => { + // display notification and mark resource as 'deleted' + this._notification.openSnackBar( + `${response.result}: ${this.resource.res.label}` + ); + this.deletedResource = true; + this._componentCommsService.emit( + new EmitEvent( + CommsEvents.resourceDeleted + ) + ); + if (this.isAnnotation) { + this.regionDeleted.emit(); + } + this._cd.markForCheck(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + break; + + case 'erase': + // erase the resource and refresh the view + this._dspApiConnection.v2.res + .eraseResource(payload) + .subscribe( + (response: DeleteResourceResponse) => { + // display notification and mark resource as 'erased' + this._notification.openSnackBar( + `${response.result}: ${this.resource.res.label}` + ); + this.deletedResource = true; + this._componentCommsService.emit( + new EmitEvent( + CommsEvents.resourceDeleted + ) + ); + // if it is an Annotation/Region which has been erases, we emit the + // regionChanged event, in order to refresh the page + if (this.isAnnotation) { + this.regionDeleted.emit(); + } + this._cd.markForCheck(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + break; + } + } else { + // update resource's label if it has changed + if (this.resource.res.label !== answer.comment) { + // get the correct lastModificationDate from the resource + this._dspApiConnection.v2.res + .getResource(this.resource.res.id) + .subscribe((res: ReadResource) => { + const payload = new UpdateResourceMetadata(); + payload.id = this.resource.res.id; + payload.type = this.resource.res.type; + payload.lastModificationDate = + res.lastModificationDate; + payload.label = answer.comment; + + 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 + if (this.isAnnotation) { + this.regionChanged.emit(); + } + this._cd.markForCheck(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage( + error + ); + } + ); + }); + } } - + } + }); + } + + /** + * display message to confirm the copy of the citation link (ARK URL) + */ + openSnackBar(message: string) { + this._notification.openSnackBar(message); + } + + /** + * called from the template when the user clicks on the add button + */ + showAddValueForm(prop: PropertyInfoValues, ev: Event) { + ev.preventDefault(); + this.propID = prop.propDef.id; + this.addValueFormIsVisible = true; + } + + /** + * called from the template when the user clicks on the cancel button + */ + hideAddValueForm() { + this.addValueFormIsVisible = false; + this.propID = undefined; + } + + /** + * given a resource property, check if an add button should be displayed under the property values + * + * @param prop the resource property + */ + addValueIsAllowed(prop: PropertyInfoValues): boolean { + // if the ontology flags this as a read-only property, + // don't ever allow to add a value + if ( + prop.propDef instanceof ResourcePropertyDefinition && + !prop.propDef.isEditable + ) { + return false; } - /** - * opens resource - * @param linkValue - */ - openResource(linkValue: ReadLinkValue | string) { - const iri = - typeof linkValue == 'string' - ? linkValue - : linkValue.linkedResourceIri; - const path = this._resourceService.getResourcePath(iri); - window.open('/resource' + path, '_blank'); - } + const isAllowed = CardinalityUtil.createValueForPropertyAllowed( + prop.propDef.id, + prop.values.length, + this.resource.res.entityInfo.classes[this.resource.res.type] + ); + + // check if: + // cardinality allows for a value to be added + // value component does not already have an add value form open + // user has write permissions + return ( + isAllowed && + this.propID !== prop.propDef.id && + this.userCanEdit + ); + } + + /** + * updates the UI in the event of a new value being added to show the new value + * + * @param valueToAdd the value to add to the end of the values array of the filtered property + */ + addValueToResource(valueToAdd: ReadValue): void { + if (this.resource.resProps) { + this.resource.resProps + .filter( + (propInfoValueArray) => + propInfoValueArray.propDef.id === valueToAdd.property + ) // filter to the correct property + .forEach((propInfoValue) => { + propInfoValue.values.push(valueToAdd); // push new value to array + }); - previewResource() { - // --> TODO: pop up resource preview on hover + if (valueToAdd instanceof ReadTextValueAsXml) { + this._updateStandoffLinkValue(); + } + } else { + // --> TODO: better error handler! + console.warn('No properties exist for this resource'); } - - openDialog(type: 'delete' | 'erase' | 'edit') { - const dialogConfig: MatDialogConfig = { - width: '560px', - maxHeight: '80vh', - position: { - top: '112px', - }, - data: { mode: type + 'Resource', title: this.resource.res.label }, - }; - - const dialogRef = this._dialog.open(DialogComponent, dialogConfig); - - dialogRef.afterClosed().subscribe((answer: ConfirmationWithComment) => { - if (!answer) { - // if the user clicks outside of the dialog window, answer is undefined - return; - } - if (answer.confirmed === true) { - if (type !== 'edit') { - const payload = new DeleteResource(); - payload.id = this.resource.res.id; - payload.type = this.resource.res.type; - payload.deleteComment = answer.comment - ? answer.comment - : undefined; - payload.lastModificationDate = this.lastModificationDate; - switch (type) { - case 'delete': - // delete the resource and refresh the view - this._dspApiConnection.v2.res - .deleteResource(payload) - .subscribe( - (response: DeleteResourceResponse) => { - // display notification and mark resource as 'deleted' - this._notification.openSnackBar( - `${response.result}: ${this.resource.res.label}` - ); - this.deletedResource = true; - this._componentCommsService.emit( - new EmitEvent( - CommsEvents.resourceDeleted - ) - ); - if (this.isAnnotation) { - this.regionDeleted.emit(); - } - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - break; - - case 'erase': - // erase the resource and refresh the view - this._dspApiConnection.v2.res - .eraseResource(payload) - .subscribe( - (response: DeleteResourceResponse) => { - // display notification and mark resource as 'erased' - this._notification.openSnackBar( - `${response.result}: ${this.resource.res.label}` - ); - this.deletedResource = true; - this._componentCommsService.emit( - new EmitEvent( - CommsEvents.resourceDeleted - ) - ); - // if it is an Annotation/Region which has been erases, we emit the - // regionChanged event, in order to refresh the page - if (this.isAnnotation) { - this.regionDeleted.emit(); - } - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - break; - } - } else { - // update resource's label if it has changed - if (this.resource.res.label !== answer.comment) { - // get the correct lastModificationDate from the resource - this._dspApiConnection.v2.res - .getResource(this.resource.res.id) - .subscribe((res: ReadResource) => { - const payload = new UpdateResourceMetadata(); - payload.id = this.resource.res.id; - payload.type = this.resource.res.type; - payload.lastModificationDate = - res.lastModificationDate; - payload.label = answer.comment; - - 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 - if (this.isAnnotation) { - this.regionChanged.emit(); - } - this._cd.markForCheck(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage( - error - ); - } - ); - }); - } - } + } + + /** + * updates the UI in the event of an existing value being updated to show the updated value + * + * @param valueToReplace the value to be replaced within the values array of the filtered property + * @param updatedValue the value to replace valueToReplace with + */ + updateValueInResource( + valueToReplace: ReadValue, + updatedValue: ReadValue + ): void { + if (this.resource.resProps && updatedValue !== null) { + this.resource.resProps + .filter( + (propInfoValueArray) => + propInfoValueArray.propDef.id === + valueToReplace.property + ) // filter to the correct property + .forEach((filteredpropInfoValueArray) => { + filteredpropInfoValueArray.values.forEach((val, index) => { + // loop through each value of the current property + if (val.id === valueToReplace.id) { + // find the value that should be updated using the id of valueToReplace + filteredpropInfoValueArray.values[index] = + updatedValue; // replace value with the updated value } + }); }); + if (updatedValue instanceof ReadTextValueAsXml) { + this._updateStandoffLinkValue(); + } + // if annotations tab is active; + if (this.isAnnotation) { + this.regionChanged.emit(); + } + } else { + console.warn('No properties exist for this resource'); } - - /** - * display message to confirm the copy of the citation link (ARK URL) - */ - openSnackBar(message: string) { - this._notification.openSnackBar(message); - } - - /** - * called from the template when the user clicks on the add button - */ - showAddValueForm(prop: PropertyInfoValues, ev: Event) { - ev.preventDefault(); - this.propID = prop.propDef.id; - this.addValueFormIsVisible = true; - } - - /** - * called from the template when the user clicks on the cancel button - */ - hideAddValueForm() { - this.addValueFormIsVisible = false; - this.propID = undefined; - } - - /** - * given a resource property, check if an add button should be displayed under the property values - * - * @param prop the resource property - */ - addValueIsAllowed(prop: PropertyInfoValues): boolean { - // if the ontology flags this as a read-only property, - // don't ever allow to add a value - if ( - prop.propDef instanceof ResourcePropertyDefinition && - !prop.propDef.isEditable - ) { - return false; - } - - const isAllowed = CardinalityUtil.createValueForPropertyAllowed( - prop.propDef.id, - prop.values.length, - this.resource.res.entityInfo.classes[this.resource.res.type] - ); - - // check if: - // cardinality allows for a value to be added - // value component does not already have an add value form open - // user has write permissions - return ( - isAllowed && - this.propID !== prop.propDef.id && - this.userCanEdit - ); + } + + /** + * given a resource property, check if its cardinality allows a value to be deleted + * + * @param prop the resource property + */ + deleteValueIsAllowed(prop: PropertyInfoValues): boolean { + const card = CardinalityUtil.deleteValueForPropertyAllowed( + prop.propDef.id, + prop.values.length, + this.resource.res.entityInfo.classes[this.resource.res.type] + ); + if (!card) { + this.cantDeleteReason = 'This value can not be deleted because it is required.'; } - /** - * updates the UI in the event of a new value being added to show the new value - * - * @param valueToAdd the value to add to the end of the values array of the filtered property - */ - addValueToResource(valueToAdd: ReadValue): void { - if (this.resource.resProps) { - this.resource.resProps - .filter( - (propInfoValueArray) => - propInfoValueArray.propDef.id === valueToAdd.property - ) // filter to the correct property - .forEach((propInfoValue) => { - propInfoValue.values.push(valueToAdd); // push new value to array - }); - - if (valueToAdd instanceof ReadTextValueAsXml) { - this._updateStandoffLinkValue(); - } - } else { - // --> TODO: better error handler! - console.warn('No properties exist for this resource'); - } + if (!this.userCanDelete) { + this.cantDeleteReason = 'You do not have teh permission to delete this value.'; } - /** - * updates the UI in the event of an existing value being updated to show the updated value - * - * @param valueToReplace the value to be replaced within the values array of the filtered property - * @param updatedValue the value to replace valueToReplace with - */ - updateValueInResource( - valueToReplace: ReadValue, - updatedValue: ReadValue - ): void { - if (this.resource.resProps && updatedValue !== null) { - this.resource.resProps - .filter( - (propInfoValueArray) => - propInfoValueArray.propDef.id === - valueToReplace.property - ) // filter to the correct property - .forEach((filteredpropInfoValueArray) => { - filteredpropInfoValueArray.values.forEach((val, index) => { - // loop through each value of the current property - if (val.id === valueToReplace.id) { - // find the value that should be updated using the id of valueToReplace - filteredpropInfoValueArray.values[index] = - updatedValue; // replace value with the updated value - } - }); - }); - if (updatedValue instanceof ReadTextValueAsXml) { + return card && this.userCanDelete; + } + + /** + * updates the UI in the event of an existing value being deleted + * + * @param valueToDelete the value to remove from the values array of the filtered property + */ + deleteValueFromResource(valueToDelete: DeleteValue): void { + if (this.resource.resProps) { + this.resource.resProps + .filter( + ( + propInfoValueArray // filter to the correct type + ) => + this._valueService.compareObjectTypeWithValueType( + propInfoValueArray.propDef.objectType, + valueToDelete.type + ) + ) + .forEach((filteredpropInfoValueArray) => { + filteredpropInfoValueArray.values.forEach((val, index) => { + // loop through each value of the current property + if (val.id === valueToDelete.id) { + // find the value that was deleted using the id + filteredpropInfoValueArray.values.splice(index, 1); // remove the value from the values array + + if (val instanceof ReadTextValueAsXml) { this._updateStandoffLinkValue(); + } } - // if annotations tab is active; - if (this.isAnnotation) { - this.regionChanged.emit(); - } - } else { - console.warn('No properties exist for this resource'); - } + }); + }); + } else { + console.warn('No properties exist for this resource'); } - - /** - * given a resource property, check if its cardinality allows a value to be deleted - * - * @param prop the resource property - */ - deleteValueIsAllowed(prop: PropertyInfoValues): boolean { - const card = CardinalityUtil.deleteValueForPropertyAllowed( - prop.propDef.id, - prop.values.length, - this.resource.res.entityInfo.classes[this.resource.res.type] + } + + toggleAllProps(status: boolean) { + this.showAllProps = !status; + localStorage.setItem('showAllProps', JSON.stringify(this.showAllProps)); + } + + /** + * gets the number of incoming links and gets the incoming links. + * @private + */ + private _getAllIncomingLinkRes() { + if (this.pageEvent) { + this.loading = true; + this._incomingService + .getIncomingLinks( + this.resource.res.id, + 0, + true ) - if (!card) { - this.cantDeleteReason = 'This value can not be deleted because it is required.'; - } - - if (!this.userCanDelete) { - this.cantDeleteReason = 'You do not have teh permission to delete this value.'; - } - - return card && this.userCanDelete; - } - - /** - * updates the UI in the event of an existing value being deleted - * - * @param valueToDelete the value to remove from the values array of the filtered property - */ - deleteValueFromResource(valueToDelete: DeleteValue): void { - if (this.resource.resProps) { - this.resource.resProps - .filter( - ( - propInfoValueArray // filter to the correct type - ) => - this._valueService.compareObjectTypeWithValueType( - propInfoValueArray.propDef.objectType, - valueToDelete.type - ) - ) - .forEach((filteredpropInfoValueArray) => { - filteredpropInfoValueArray.values.forEach((val, index) => { - // loop through each value of the current property - if (val.id === valueToDelete.id) { - // find the value that was deleted using the id - filteredpropInfoValueArray.values.splice(index, 1); // remove the value from the values array - - if (val instanceof ReadTextValueAsXml) { - this._updateStandoffLinkValue(); - } - } - }); - }); - } else { - console.warn('No properties exist for this resource'); - } - } - - toggleAllProps(status: boolean) { - this.showAllProps = !status; - localStorage.setItem('showAllProps', JSON.stringify(this.showAllProps)); - } - - /** - * gets the number of incoming links and gets the incoming links. - * @private - */ - private _getAllIncomingLinkRes() { - if (this.pageEvent) { - this.loading = true; - this._incomingService - .getIncomingLinks( - this.resource.res.id, - 0, - true - ) - .subscribe((response: CountQueryResponse) => { - this.numberOffAllIncomingLinkRes = response.numberOfResults; - const round = this.numberOffAllIncomingLinkRes > this.amount_resources ? - Math.ceil(this.numberOffAllIncomingLinkRes/this.amount_resources) : 1; - - const arr = new Array>(round); - - for (let i = 0; i < round ; i++) { - arr[i] = this._incomingService.getIncomingLinks(this.resource.res.id, i); - } - - forkJoin(arr) - .subscribe((data: ReadResourceSequence[]) => { - const flattenIncomingRes = data.flatMap((a) => a.resources); - this.allIncomingLinkResources = this._sortingService.keySortByAlphabetical( - flattenIncomingRes, - "resourceClassLabel", - "label" - ); - - this._getDisplayedIncomingLinkRes(); - this.loading = false; - this._cd.markForCheck(); - }, () => { - this.loading = false; - }); - },() => { - this.loading = false; + .subscribe((response: CountQueryResponse) => { + this.numberOffAllIncomingLinkRes = response.numberOfResults; + const round = this.numberOffAllIncomingLinkRes > this.amount_resources ? + Math.ceil(this.numberOffAllIncomingLinkRes / this.amount_resources) : 1; + + const arr = new Array>(round); + + for (let i = 0; i < round; i++) { + arr[i] = this._incomingService.getIncomingLinks(this.resource.res.id, i); + } + + forkJoin(arr) + .subscribe((data: ReadResourceSequence[]) => { + const flattenIncomingRes = data.flatMap((a) => a.resources); + this.allIncomingLinkResources = this._sortingService.keySortByAlphabetical( + flattenIncomingRes, + 'resourceClassLabel', + 'label' + ); + + this._getDisplayedIncomingLinkRes(); + this.loading = false; + this._cd.markForCheck(); + }, () => { + this.loading = false; }); - } + }, () => { + this.loading = false; + }); } - - private _getDisplayedIncomingLinkRes() { - const startIndex = this.pageEvent.pageIndex * this.amount_resources; - this.displayedIncomingLinkResources = this.allIncomingLinkResources.slice(startIndex, startIndex + this.amount_resources); + } + + private _getDisplayedIncomingLinkRes() { + const startIndex = this.pageEvent.pageIndex * this.amount_resources; + this.displayedIncomingLinkResources = this.allIncomingLinkResources.slice(startIndex, startIndex + this.amount_resources); + } + + /** + * updates the standoff link value for the resource being displayed. + * + */ + private _updateStandoffLinkValue(): void { + if (this.resource.res === undefined) { + // this should never happen: + // if the user was able to click on a standoff link, + // then the resource must have been initialised before. + return; } - /** - * updates the standoff link value for the resource being displayed. - * - */ - private _updateStandoffLinkValue(): void { - if (this.resource.res === undefined) { - // this should never happen: - // if the user was able to click on a standoff link, - // then the resource must have been initialised before. - return; - } - - const gravsearchQuery = ` + const gravsearchQuery = ` PREFIX knora-api: CONSTRUCT { ?res knora-api:isMainResource true . @@ -747,52 +738,52 @@ export class PropertiesComponent implements OnInit, OnChanges, OnDestroy { OFFSET 0 `; - this._dspApiConnection.v2.search - .doExtendedSearch(gravsearchQuery) - .subscribe( - (res: ReadResourceSequence) => { - // one resource is expected - if (res.resources.length !== 1) { - return; - } - - const newStandoffLinkVals = res.resources[0].getValuesAs( - Constants.HasStandoffLinkToValue, - ReadLinkValue - ); - - this.resource.resProps - .filter( - (resPropInfoVal) => - resPropInfoVal.propDef.id === - Constants.HasStandoffLinkToValue - ) - .forEach((standoffLinkResPropInfoVal) => { - // delete all the existing standoff link values - standoffLinkResPropInfoVal.values = []; - // push standoff link values retrieved for the resource - newStandoffLinkVals.forEach((standoffLinkVal) => { - standoffLinkResPropInfoVal.values.push( - standoffLinkVal - ); - }); - }); - this._cd.markForCheck(); - }, - (err) => { - console.error(err); - } - ); - } - - private _getLastModificationDate(resId: string) { - this._dspApiConnection.v2.res - .getResource(resId) - .subscribe( - (res: ReadResource) => { - this.lastModificationDate = res.lastModificationDate; - this._cd.markForCheck(); - } - ); - } + this._dspApiConnection.v2.search + .doExtendedSearch(gravsearchQuery) + .subscribe( + (res: ReadResourceSequence) => { + // one resource is expected + if (res.resources.length !== 1) { + return; + } + + const newStandoffLinkVals = res.resources[0].getValuesAs( + Constants.HasStandoffLinkToValue, + ReadLinkValue + ); + + this.resource.resProps + .filter( + (resPropInfoVal) => + resPropInfoVal.propDef.id === + Constants.HasStandoffLinkToValue + ) + .forEach((standoffLinkResPropInfoVal) => { + // delete all the existing standoff link values + standoffLinkResPropInfoVal.values = []; + // push standoff link values retrieved for the resource + newStandoffLinkVals.forEach((standoffLinkVal) => { + standoffLinkResPropInfoVal.values.push( + standoffLinkVal + ); + }); + }); + this._cd.markForCheck(); + }, + (err) => { + console.error(err); + } + ); + } + + private _getLastModificationDate(resId: string) { + this._dspApiConnection.v2.res + .getResource(resId) + .subscribe( + (res: ReadResource) => { + this.lastModificationDate = res.lastModificationDate; + this._cd.markForCheck(); + } + ); + } } 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 7ddbe95064..b755ef5e1d 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 @@ -62,7 +62,7 @@ export class TextComponent implements OnInit, AfterViewInit { .getFileInfo(this.src.fileValue.fileUrl) .subscribe( res => this.originalFilename = res['originalFilename'], - error => this.failedToLoad = true + () => this.failedToLoad = true ); } diff --git a/apps/dsp-app/src/app/workspace/resource/values/color-value/color-picker/color-picker.component.ts b/apps/dsp-app/src/app/workspace/resource/values/color-value/color-picker/color-picker.component.ts index d7dedf635e..863b53c7e6 100644 --- a/apps/dsp-app/src/app/workspace/resource/values/color-value/color-picker/color-picker.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/values/color-value/color-picker/color-picker.component.ts @@ -192,7 +192,8 @@ export class ColorPickerComponent this.placeholder = 'Click to select a color'; } - // eslint-disable-next-line @typescript-eslint/no-empty-function + + // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars onChange = (_: any) => {}; // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/apps/dsp-app/src/app/workspace/resource/values/date-value/date-value-handler/date-value-handler.component.ts b/apps/dsp-app/src/app/workspace/resource/values/date-value/date-value-handler/date-value-handler.component.ts index 146fd13c04..34b800f655 100644 --- a/apps/dsp-app/src/app/workspace/resource/values/date-value/date-value-handler/date-value-handler.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/values/date-value/date-value-handler/date-value-handler.component.ts @@ -296,7 +296,8 @@ export class DateValueHandlerComponent endDate: this.endDate, }); } - // eslint-disable-next-line @typescript-eslint/no-empty-function + + // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars onChange = (_: any) => {}; // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -364,10 +365,11 @@ export class DateValueHandlerComponent ev.preventDefault(); this.isPeriodControl.setValue(!this.isPeriodControl.value); } - // eslint-disable-next-line @typescript-eslint/no-empty-function + + // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars onContainerClick(event: MouseEvent): void {} - // eslint-disable-next-line @typescript-eslint/no-empty-function + // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars setDescribedByIds(ids: string[]): void {} /* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/apps/dsp-app/src/app/workspace/resource/values/interval-value/interval-input/interval-input.component.ts b/apps/dsp-app/src/app/workspace/resource/values/interval-value/interval-input/interval-input.component.ts index 279892a2e0..17bdc0ca62 100644 --- a/apps/dsp-app/src/app/workspace/resource/values/interval-value/interval-input/interval-input.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/values/interval-value/interval-input/interval-input.component.ts @@ -128,7 +128,7 @@ export class IntervalInputComponent @Input() intervalStartLabel = 'start'; @Input() intervalEndLabel = 'end'; @Input() valueRequiredValidator = true; - // eslint-disable-next-line @typescript-eslint/no-empty-function + // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars onChange = (_: any) => {}; // eslint-disable-next-line @typescript-eslint/no-empty-function onTouched = () => {}; 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 bd484caf22..b8124ced5c 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 @@ -1,204 +1,197 @@ -import { - Component, - Inject, - Input, - OnChanges, - OnDestroy, - OnInit, - ViewChild, -} from '@angular/core'; +import { Component, Inject, Input, OnChanges, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { MatMenuTrigger } from '@angular/material/menu'; import { - ApiResponseError, - CreateListValue, - KnoraApiConnection, - ListNodeV2, - ReadListValue, - ResourcePropertyDefinition, - UpdateListValue, + ApiResponseError, + CreateListValue, + KnoraApiConnection, + ListNodeV2, + ReadListValue, + ResourcePropertyDefinition, + UpdateListValue } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { BaseValueDirective } from '@dsp-app/src/app/main/directive/base-value.directive'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; + @Component({ - selector: 'app-list-value', - templateUrl: './list-value.component.html', - styleUrls: ['./list-value.component.scss'], + selector: 'app-list-value', + templateUrl: './list-value.component.html', + styleUrls: ['./list-value.component.scss'] }) export class ListValueComponent - extends BaseValueDirective - implements OnInit, OnChanges, OnDestroy -{ - @Input() displayValue?: ReadListValue; - @Input() propertyDef: ResourcePropertyDefinition; - @ViewChild(MatMenuTrigger) menuTrigger: MatMenuTrigger; - - listRootNode: ListNodeV2; - // active node - selectedNode: ListNodeV2; - - customValidators = []; - - selectedNodeHierarchy: string[] = []; - - constructor( - @Inject(FormBuilder) protected _fb: FormBuilder, - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler - ) { - super(); + extends BaseValueDirective + implements OnInit, OnChanges, OnDestroy { + @Input() displayValue?: ReadListValue; + @Input() propertyDef: ResourcePropertyDefinition; + @ViewChild(MatMenuTrigger) menuTrigger: MatMenuTrigger; + + listRootNode: ListNodeV2; + // active node + selectedNode: ListNodeV2; + + customValidators = []; + + selectedNodeHierarchy: string[] = []; + + constructor( + @Inject(FormBuilder) protected _fb: FormBuilder, + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _errorHandler: AppErrorHandler + ) { + super(); + } + + getInitValue(): string | null { + if (this.displayValue !== undefined) { + this.getReadModeValue(this.displayValue.listNode); + return this.displayValue.listNode; + } else { + return null; } - - getInitValue(): string | null { - if (this.displayValue !== undefined) { - this.getReadModeValue(this.displayValue.listNode); - return this.displayValue.listNode; - } else { - return null; - } + } + + // override the resetFormControl() from the base component to deal with appending root nodes. + resetFormControl(): void { + super.resetFormControl(); + if (this.mode === 'update') { + this.selectedNode = new ListNodeV2(); + this.selectedNode.label = this.displayValue.listNodeLabel; + } else { + this.selectedNode = null; } - // override the resetFormControl() from the base component to deal with appending root nodes. - resetFormControl(): void { - super.resetFormControl(); - if (this.mode === 'update') { - this.selectedNode = new ListNodeV2(); - this.selectedNode.label = this.displayValue.listNodeLabel; - } else { - this.selectedNode = null; - } - if (this.valueFormControl !== undefined) { - if (this.mode !== 'read') { - 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); - } - ); - } - } else { - this.valueFormControl.setValue(this.displayValue.listNodeLabel); - } + if (this.valueFormControl !== undefined) { + if (this.mode !== 'read') { + 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); + } + ); } + } else { + this.valueFormControl.setValue(this.displayValue.listNodeLabel); + } } + } - ngOnInit() { - super.ngOnInit(); - } - - ngOnChanges(): void { - this.resetFormControl(); - } - - ngOnDestroy(): void { - super.ngOnDestroy(); - } - - getNewValue(): CreateListValue | false { - if (this.mode !== 'create' || !this.form.valid || this.isEmptyVal()) { - return false; - } + ngOnInit() { + super.ngOnInit(); + } - const newListValue = new CreateListValue(); - newListValue.listNode = this.valueFormControl.value; + ngOnChanges(): void { + this.resetFormControl(); + } - if ( - this.commentFormControl.value !== null && - this.commentFormControl.value !== '' - ) { - newListValue.valueHasComment = this.commentFormControl.value; - } + ngOnDestroy(): void { + super.ngOnDestroy(); + } - return newListValue; + getNewValue(): CreateListValue | false { + if (this.mode !== 'create' || !this.form.valid || this.isEmptyVal()) { + return false; } - getUpdatedValue(): UpdateListValue | false { - if (this.mode !== 'update' || !this.form.valid) { - return false; - } + const newListValue = new CreateListValue(); + newListValue.listNode = this.valueFormControl.value; - const updatedListValue = new UpdateListValue(); + if ( + this.commentFormControl.value !== null && + this.commentFormControl.value !== '' + ) { + newListValue.valueHasComment = this.commentFormControl.value; + } - updatedListValue.id = this.displayValue.id; - if (this.selectedNode.id !== '') { - updatedListValue.listNode = this.selectedNode.id; - } else { - updatedListValue.listNode = this.displayValue.listNode; - } - if ( - this.commentFormControl.value !== null && - this.commentFormControl.value !== '' - ) { - updatedListValue.valueHasComment = this.commentFormControl.value; - } + return newListValue; + } - return updatedListValue; + getUpdatedValue(): UpdateListValue | false { + if (this.mode !== 'update' || !this.form.valid) { + return false; } - getSelectedNode(item: ListNodeV2) { - this.menuTrigger.closeMenu(); - this.valueFormControl.markAsDirty(); - this.selectedNode = item; - this.valueFormControl.setValue(item.id); - } + const updatedListValue = new UpdateListValue(); - getReadModeValue(nodeIri: string): void { - 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 - ); - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } + updatedListValue.id = this.displayValue.id; + if (this.selectedNode.id !== '') { + updatedListValue.listNode = this.selectedNode.id; + } else { + updatedListValue.listNode = this.displayValue.listNode; + } + if ( + this.commentFormControl.value !== null && + this.commentFormControl.value !== '' + ) { + updatedListValue.valueHasComment = this.commentFormControl.value; } - _getHierarchy(selectedNodeIri: string, children: ListNodeV2[]): string[] { - for (let i = 0; i < children.length; i++) { - const node = children[i]; - if (node.id !== selectedNodeIri) { - if (node.children) { - const path = this._getHierarchy( - selectedNodeIri, - node.children - ); - - if (path) { - path.unshift(node.label); - return path; - } - } + return updatedListValue; + } + + getSelectedNode(item: ListNodeV2) { + this.menuTrigger.closeMenu(); + this.valueFormControl.markAsDirty(); + this.selectedNode = item; + this.valueFormControl.setValue(item.id); + } + + getReadModeValue(nodeIri: string): void { + 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 { - return [node.label]; + this.selectedNodeHierarchy = this._getHierarchy( + nodeIri, + response.children + ); } + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } + } + + _getHierarchy(selectedNodeIri: string, children: ListNodeV2[]): string[] { + for (let i = 0; i < children.length; i++) { + const node = children[i]; + if (node.id !== selectedNodeIri) { + if (node.children) { + const path = this._getHierarchy( + selectedNodeIri, + node.children + ); + + if (path) { + path.unshift(node.label); + return path; + } } + } else { + return [node.label]; + } } + } } diff --git a/apps/dsp-app/src/app/workspace/resource/values/time-value/time-input/time-input.component.ts b/apps/dsp-app/src/app/workspace/resource/values/time-value/time-input/time-input.component.ts index 83584115e6..ffcad0e5f9 100644 --- a/apps/dsp-app/src/app/workspace/resource/values/time-value/time-input/time-input.component.ts +++ b/apps/dsp-app/src/app/workspace/resource/values/time-value/time-input/time-input.component.ts @@ -113,7 +113,7 @@ export class TimeInputComponent errorState = false; controlType = 'app-time-input'; matcher = new ValueErrorStateMatcher(); - // eslint-disable-next-line @typescript-eslint/no-empty-function + // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars onChange = (_: any) => {}; // eslint-disable-next-line @typescript-eslint/no-empty-function onTouched = () => {}; diff --git a/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.spec.ts b/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.spec.ts index f946fac0f3..972ea9d849 100644 --- a/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.spec.ts +++ b/apps/dsp-app/src/app/workspace/search/fulltext-search/fulltext-search.component.spec.ts @@ -173,10 +173,6 @@ describe('FulltextSearchComponent', () => { it('should get projects on init', () => { const projSpy = TestBed.inject(DspApiConnectionToken); - expect( - projSpy.admin.projectsEndpoint.getProjects - ).toHaveBeenCalledTimes(1); - expect(testHostComponent.fulltextSearch.projects).toBeDefined(); expect(testHostComponent.fulltextSearch.projects.length).toEqual(5); expect(testHostComponent.fulltextSearch.projectfilter).toEqual(true); 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 d676b563dd..4224cf9891 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 @@ -1,10 +1,4 @@ -import { - ConnectionPositionPair, - Overlay, - OverlayConfig, - OverlayRef, - PositionStrategy, -} from '@angular/cdk/overlay'; +import { ConnectionPositionPair, Overlay, OverlayConfig, OverlayRef, PositionStrategy } from '@angular/cdk/overlay'; import { TemplatePortal } from '@angular/cdk/portal'; import { Component, @@ -18,481 +12,471 @@ import { Output, TemplateRef, ViewChild, - ViewContainerRef, + ViewContainerRef } from '@angular/core'; import { MatMenuTrigger } from '@angular/material/menu'; -import { - ApiResponseData, - ApiResponseError, - Constants, - KnoraApiConnection, - ProjectResponse, - ProjectsResponse, - ReadProject, -} from '@dasch-swiss/dsp-js'; +import { ApiResponseError, Constants, KnoraApiConnection, ReadProject } from '@dasch-swiss/dsp-js'; import { Subscription } from 'rxjs'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { ComponentCommunicationEventService, - Events, + Events } from '@dsp-app/src/app/main/services/component-communication-event.service'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; import { SearchParams } from '../../results/list-view/list-view.component'; import { SortingService } from '@dasch-swiss/vre/shared/app-helper-services'; +import { ProjectApiService } from '@dasch-swiss/vre/shared/app-api'; export interface PrevSearchItem { - projectIri?: string; - projectLabel?: string; - query: string; + projectIri?: string; + projectLabel?: string; + query: string; } const resolvedPromise = Promise.resolve(null); @Component({ - selector: 'app-fulltext-search', - templateUrl: './fulltext-search.component.html', - styleUrls: ['./fulltext-search.component.scss'], + selector: 'app-fulltext-search', + templateUrl: './fulltext-search.component.html', + styleUrls: ['./fulltext-search.component.scss'] }) export class FulltextSearchComponent implements OnInit, OnChanges, OnDestroy { - /** - * - * @param [projectfilter] If true it shows the selection - * of projects to filter by one of them - */ - @Input() projectfilter?: boolean = false; - - /** - * filter ontologies in advanced search or query in fulltext search by specified project IRI - * - * @param limitToProject - */ - @Input() limitToProject?: string; - - /** - * emits selected project in case of projectfilter - */ - @Output() limitToProjectChange = new EventEmitter(); - - /** - * the data event emitter of type SearchParams - * - * @param search - */ - @Output() search = new EventEmitter(); - - @ViewChild('fulltextSearchPanel', { static: false }) - searchPanel: ElementRef; - - @ViewChild('fulltextSearchInput', { static: false }) - searchInput: ElementRef; - @ViewChild('fulltextSearchInputMobile', { static: false }) - searchInputMobile: ElementRef; - - @ViewChild('fulltextSearchMenu', { static: false }) - searchMenu: TemplateRef; - - @ViewChild('btnToSelectProject', { static: false }) - selectProject: MatMenuTrigger; - - // search query - searchQuery: string; - - // previous search = full-text search history - prevSearch: PrevSearchItem[]; - - // list of projects, in case of filterproject is true - projects: ReadProject[]; - - // selected project, in case of limitToProject and/or projectfilter is true - project: ReadProject; - - defaultProjectLabel = 'All projects'; - - projectLabel: string = this.defaultProjectLabel; - - projectIri: string; + /** + * + * @param [projectfilter] If true it shows the selection + * of projects to filter by one of them + */ + @Input() projectfilter?: boolean = false; + + /** + * filter ontologies in advanced search or query in fulltext search by specified project IRI + * + * @param limitToProject + */ + @Input() limitToProject?: string; + + /** + * emits selected project in case of projectfilter + */ + @Output() limitToProjectChange = new EventEmitter(); + + /** + * the data event emitter of type SearchParams + * + * @param search + */ + @Output() search = new EventEmitter(); + + @ViewChild('fulltextSearchPanel', { static: false }) + searchPanel: ElementRef; + + @ViewChild('fulltextSearchInput', { static: false }) + searchInput: ElementRef; + @ViewChild('fulltextSearchInputMobile', { static: false }) + searchInputMobile: ElementRef; + + @ViewChild('fulltextSearchMenu', { static: false }) + searchMenu: TemplateRef; + + @ViewChild('btnToSelectProject', { static: false }) + selectProject: MatMenuTrigger; + + // search query + searchQuery: string; + + // previous search = full-text search history + prevSearch: PrevSearchItem[]; + + // list of projects, in case of filterproject is true + projects: ReadProject[]; + + // selected project, in case of limitToProject and/or projectfilter is true + project: ReadProject; + + defaultProjectLabel = 'All projects'; + + projectLabel: string = this.defaultProjectLabel; + + projectIri: string; + + componentCommsSubscriptions: Subscription[] = []; + + // in case of an (api) error + error: any; + + // is search panel focused? + searchPanelFocus = false; + + // overlay reference + overlayRef: OverlayRef; + + // do not show the following projects: default system projects from knora + hiddenProjects: string[] = [ + Constants.SystemProjectIRI, + Constants.DefaultSharedOntologyIRI + ]; + + // toggle phone panel + 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, + private _notification: NotificationService + ) { + } + + ngOnInit(): void { + // on page refresh, split the url into an array of strings and assign the `searchQuery` to the last element of this array of strings + // this persists the search term in the search input field + const urlArray = window.location.pathname.split('/'); + const currentSearchTerm = urlArray[urlArray.length - 1]; + if (urlArray[urlArray.length - 2] === 'fulltext') { + this.searchQuery = decodeURI(decodeURI(currentSearchTerm)); + } - componentCommsSubscriptions: Subscription[] = []; + // initialise prevSearch + const prevSearchOption = JSON.parse(localStorage.getItem('prevSearch')); + if (prevSearchOption !== null) { + this.prevSearch = prevSearchOption; + } else { + this.prevSearch = []; + } - // in case of an (api) error - error: any; + if (this.limitToProject) { + this.getProject(this.limitToProject); + } - // is search panel focused? - searchPanelFocus = false; + if (this.projectfilter) { + this.getAllProjects(); + } - // overlay reference - overlayRef: OverlayRef; + // in the event of a grav search (advanced or expert search), clear the input field + this.componentCommsSubscriptions.push( + this._componentCommsService.on(Events.gravSearchExecuted, () => { + this.searchQuery = null; + }) + ); + + this.componentCommsSubscriptions.push( + this._componentCommsService.on(Events.projectCreated, () => { + this.getAllProjects(); + }) + ); + } + + ngOnChanges() { + // resource classes have been reinitialized + // reset form + resolvedPromise.then(() => { + if (localStorage.getItem('currentProject') !== null) { + this.setProject( + JSON.parse(localStorage.getItem('currentProject')) + ); + } + }); + } + + ngOnDestroy() { + // unsubscribe from all subscriptions incomponentCommsSubscriptions when component is destroyed + if (this.componentCommsSubscriptions !== undefined) { + this.componentCommsSubscriptions.forEach((sub) => + sub.unsubscribe() + ); + } + } + + /** + * get all public projects from DSP-API + */ + getAllProjects(): void { + this._projectsApiService.list().subscribe( + (response) => { + // filter out deactivated projects and system projects + this.projects = response.projects.filter( + (p) => + p.status === true && !this.hiddenProjects.includes(p.id) + ); - // do not show the following projects: default system projects from knora - hiddenProjects: string[] = [ - Constants.SystemProjectIRI, - Constants.DefaultSharedOntologyIRI, + if (localStorage.getItem('currentProject') !== null) { + this.project = JSON.parse( + localStorage.getItem('currentProject') + ); + } + this.projects = this._sortingService.keySortByAlphabetical( + this.projects, + 'shortname' + ); + }, + (error: ApiResponseError) => { + this.error = error; + this._errorHandler.showMessage(error); + } + ); + } + + /** + * get project by IRI + * @param id Project Id + */ + getProject(id: string): void { + this._projectsApiService.get(id) + .subscribe( + response => { + this.setProject(response.project); + }); + } + + /** + * set current project and switch focus to input field. + * @params project + */ + setProject(project?: ReadProject): void { + if (!project) { + // set default project: all + this.projectLabel = this.defaultProjectLabel; + this.projectIri = undefined; + this.limitToProject = undefined; + this.limitToProjectChange.emit(this.limitToProject); + localStorage.removeItem('currentProject'); + } else { + // set current project shortname and id + this.projectLabel = project.shortname; + this.projectIri = project.id; + this.limitToProject = project.id; + this.limitToProjectChange.emit(this.limitToProject); + localStorage.setItem('currentProject', JSON.stringify(project)); + } + } + + /** + * open the search panel with backdrop + */ + openPanelWithBackdrop(): void { + const config = new OverlayConfig({ + hasBackdrop: true, + backdropClass: 'cdk-overlay-transparent-backdrop', + positionStrategy: this.getOverlayPosition(), + scrollStrategy: this._overlay.scrollStrategies.block() + }); + + this.overlayRef = this._overlay.create(config); + this.overlayRef.attach( + new TemplatePortal(this.searchMenu, this._viewContainerRef) + ); + this.overlayRef.backdropClick().subscribe(() => { + this.searchPanelFocus = false; + if (this.overlayRef) { + this.overlayRef.detach(); + } + }); + } + + /** + * return the correct overlay position + */ + getOverlayPosition(): PositionStrategy { + const positions = [ + new ConnectionPositionPair( + { originX: 'start', originY: 'bottom' }, + { overlayX: 'start', overlayY: 'top' } + ), + new ConnectionPositionPair( + { originX: 'start', originY: 'top' }, + { overlayX: 'start', overlayY: 'bottom' } + ) ]; - // toggle phone panel - displayPhonePanel = false; - - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _componentCommsService: ComponentCommunicationEventService, - private _errorHandler: AppErrorHandler, - private _overlay: Overlay, - private _sortingService: SortingService, - private _viewContainerRef: ViewContainerRef, - private _notification: NotificationService - ) {} - - ngOnInit(): void { - // on page refresh, split the url into an array of strings and assign the `searchQuery` to the last element of this array of strings - // this persists the search term in the search input field - const urlArray = window.location.pathname.split('/'); - const currentSearchTerm = urlArray[urlArray.length - 1]; - if (urlArray[urlArray.length - 2] === 'fulltext') { - this.searchQuery = decodeURI(decodeURI(currentSearchTerm)); + // tslint:disable-next-line: max-line-length + const overlayPosition = this._overlay + .position() + .flexibleConnectedTo(this.searchPanel) + .withPositions(positions) + .withLockedPosition(false); + + return overlayPosition; + } + + /** + * send the search query to parent and store the new query in the local storage + * to have a search history list + */ + doSearch(): void { + if (this.searchQuery !== undefined && this.searchQuery !== null) { + // search query must be at least 3 characters + if (this.searchQuery.length < 3) { + // show the error message if the user entered at least 1 character + if (this.searchQuery !== '') { + this._notification.openSnackBar( + 'Search query must be at least 3 characters long.', + 'error' + ); } - // initialise prevSearch - const prevSearchOption = JSON.parse(localStorage.getItem('prevSearch')); - if (prevSearchOption !== null) { - this.prevSearch = prevSearchOption; - } else { - this.prevSearch = []; + return; + } + + // push the search query into the local storage prevSearch array (previous search) + // to have a list of recent search requests + let existingPrevSearch: PrevSearchItem[] = JSON.parse( + localStorage.getItem('prevSearch') + ); + if (existingPrevSearch === null) { + existingPrevSearch = []; + } + let i = 0; + for (const entry of existingPrevSearch) { + // remove entry, if exists already + if ( + this.searchQuery === entry.query && + this.projectIri === entry.projectIri + ) { + existingPrevSearch.splice(i, 1); } + i++; + } - if (this.limitToProject) { - this.getProject(this.limitToProject); - } + // a search value is expected to have at least length of 3 + if (this.searchQuery.length > 2) { + let currentQuery: PrevSearchItem = { + query: this.searchQuery + }; - if (this.projectfilter) { - this.getAllProjects(); + if (this.projectIri) { + currentQuery = { + projectIri: this.projectIri, + projectLabel: this.projectLabel, + query: this.searchQuery + }; } - // in the event of a grav search (advanced or expert search), clear the input field - this.componentCommsSubscriptions.push( - this._componentCommsService.on(Events.gravSearchExecuted, () => { - this.searchQuery = null; - }) - ); + existingPrevSearch.push(currentQuery); - this.componentCommsSubscriptions.push( - this._componentCommsService.on(Events.projectCreated, () => { - this.getAllProjects(); - }) + localStorage.setItem( + 'prevSearch', + JSON.stringify(existingPrevSearch) ); - } + } - ngOnChanges() { - // resource classes have been reinitialized - // reset form - resolvedPromise.then(() => { - if (localStorage.getItem('currentProject') !== null) { - this.setProject( - JSON.parse(localStorage.getItem('currentProject')) - ); - } - }); + this.emitSearchParams(); } - ngOnDestroy() { - // unsubscribe from all subscriptions incomponentCommsSubscriptions when component is destroyed - if (this.componentCommsSubscriptions !== undefined) { - this.componentCommsSubscriptions.forEach((sub) => - sub.unsubscribe() - ); - } - } + this.resetSearch(); - /** - * get all public projects from DSP-API - */ - getAllProjects(): void { - this._dspApiConnection.admin.projectsEndpoint.getProjects().subscribe( - (response: ApiResponseData) => { - // filter out deactivated projects and system projects - this.projects = response.body.projects.filter( - (p) => - p.status === true && !this.hiddenProjects.includes(p.id) - ); - - if (localStorage.getItem('currentProject') !== null) { - this.project = JSON.parse( - localStorage.getItem('currentProject') - ); - } - this.projects = this._sortingService.keySortByAlphabetical( - this.projects, - 'shortname' - ); - }, - (error: ApiResponseError) => { - this.error = error; - this._errorHandler.showMessage(error); - } - ); + if (this.overlayRef) { + this.overlayRef.detach(); } - - /** - * get project by IRI - * @param id Project Id - */ - getProject(id: string): void { - this._dspApiConnection.admin.projectsEndpoint - .getProjectByIri(id) - .subscribe( - (project: ApiResponseData) => { - this.setProject(project.body.project); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + } + + /** + * clear the whole list of search + */ + resetSearch(): void { + if (this.displayPhonePanel) { + this.searchInputMobile.nativeElement.blur(); + this.togglePhonePanel(); + } else { + this.searchPanelFocus = false; + this.searchInput.nativeElement.blur(); } - - /** - * set current project and switch focus to input field. - * @params project - */ - setProject(project?: ReadProject): void { - if (!project) { - // set default project: all - this.projectLabel = this.defaultProjectLabel; - this.projectIri = undefined; - this.limitToProject = undefined; - this.limitToProjectChange.emit(this.limitToProject); - localStorage.removeItem('currentProject'); - } else { - // set current project shortname and id - this.projectLabel = project.shortname; - this.projectIri = project.id; - this.limitToProject = project.id; - this.limitToProjectChange.emit(this.limitToProject); - localStorage.setItem('currentProject', JSON.stringify(project)); - } + if (this.overlayRef) { + this.overlayRef.detach(); } - - /** - * open the search panel with backdrop - */ - openPanelWithBackdrop(): void { - const config = new OverlayConfig({ - hasBackdrop: true, - backdropClass: 'cdk-overlay-transparent-backdrop', - positionStrategy: this.getOverlayPosition(), - scrollStrategy: this._overlay.scrollStrategies.block(), - }); - - this.overlayRef = this._overlay.create(config); - this.overlayRef.attach( - new TemplatePortal(this.searchMenu, this._viewContainerRef) - ); - this.overlayRef.backdropClick().subscribe(() => { - this.searchPanelFocus = false; - if (this.overlayRef) { - this.overlayRef.detach(); - } - }); + } + + /** + * set the focus on the search panel + */ + setFocus(): void { + if (localStorage.getItem('prevSearch') !== null) { + this.prevSearch = this._sortingService.reverseArray( + JSON.parse(localStorage.getItem('prevSearch')) + ); + } else { + this.prevSearch = []; } - /** - * return the correct overlay position - */ - getOverlayPosition(): PositionStrategy { - const positions = [ - new ConnectionPositionPair( - { originX: 'start', originY: 'bottom' }, - { overlayX: 'start', overlayY: 'top' } - ), - new ConnectionPositionPair( - { originX: 'start', originY: 'top' }, - { overlayX: 'start', overlayY: 'bottom' } - ), - ]; - - // tslint:disable-next-line: max-line-length - const overlayPosition = this._overlay - .position() - .flexibleConnectedTo(this.searchPanel) - .withPositions(positions) - .withLockedPosition(false); - - return overlayPosition; + if (!this.displayPhonePanel) { + this.searchPanelFocus = true; + this.openPanelWithBackdrop(); } - - /** - * send the search query to parent and store the new query in the local storage - * to have a search history list - */ - doSearch(): void { - if (this.searchQuery !== undefined && this.searchQuery !== null) { - // search query must be at least 3 characters - if (this.searchQuery.length < 3) { - // show the error message if the user entered at least 1 character - if (this.searchQuery !== '') { - this._notification.openSnackBar( - 'Search query must be at least 3 characters long.', - 'error' - ); - } - - return; - } - - // push the search query into the local storage prevSearch array (previous search) - // to have a list of recent search requests - let existingPrevSearch: PrevSearchItem[] = JSON.parse( - localStorage.getItem('prevSearch') - ); - if (existingPrevSearch === null) { - existingPrevSearch = []; - } - let i = 0; - for (const entry of existingPrevSearch) { - // remove entry, if exists already - if ( - this.searchQuery === entry.query && - this.projectIri === entry.projectIri - ) { - existingPrevSearch.splice(i, 1); - } - i++; - } - - // a search value is expected to have at least length of 3 - if (this.searchQuery.length > 2) { - let currentQuery: PrevSearchItem = { - query: this.searchQuery, - }; - - if (this.projectIri) { - currentQuery = { - projectIri: this.projectIri, - projectLabel: this.projectLabel, - query: this.searchQuery, - }; - } - - existingPrevSearch.push(currentQuery); - - localStorage.setItem( - 'prevSearch', - JSON.stringify(existingPrevSearch) - ); - } - - this.emitSearchParams(); - } - - this.resetSearch(); - - if (this.overlayRef) { - this.overlayRef.detach(); - } - } - - /** - * clear the whole list of search - */ - resetSearch(): void { - if (this.displayPhonePanel) { - this.searchInputMobile.nativeElement.blur(); - this.togglePhonePanel(); - } else { - this.searchPanelFocus = false; - this.searchInput.nativeElement.blur(); - } - if (this.overlayRef) { - this.overlayRef.detach(); - } + } + + /** + * perform a search with a selected search item from the search history + * @params prevSearch + */ + doPrevSearch(prevSearch: PrevSearchItem): void { + this.searchQuery = prevSearch.query; + + if (prevSearch.projectIri !== undefined) { + this.projectIri = prevSearch.projectIri; + this.projectLabel = prevSearch.projectLabel; + } else { + this.projectIri = undefined; + this.projectLabel = this.defaultProjectLabel; } + this.emitSearchParams(); - /** - * set the focus on the search panel - */ - setFocus(): void { - if (localStorage.getItem('prevSearch') !== null) { - this.prevSearch = this._sortingService.reverseArray( - JSON.parse(localStorage.getItem('prevSearch')) - ); - } else { - this.prevSearch = []; - } + this.resetSearch(); - if (!this.displayPhonePanel) { - this.searchPanelFocus = true; - this.openPanelWithBackdrop(); - } + if (this.overlayRef) { + this.overlayRef.detach(); } - - /** - * perform a search with a selected search item from the search history - * @params prevSearch - */ - doPrevSearch(prevSearch: PrevSearchItem): void { - this.searchQuery = prevSearch.query; - - if (prevSearch.projectIri !== undefined) { - this.projectIri = prevSearch.projectIri; - this.projectLabel = prevSearch.projectLabel; - } else { - this.projectIri = undefined; - this.projectLabel = this.defaultProjectLabel; - } - this.emitSearchParams(); - - this.resetSearch(); - - if (this.overlayRef) { - this.overlayRef.detach(); - } + } + + /** + * remove one search item from the search history + * @params prevSearchItem + */ + resetPrevSearch(prevSearchItem?: PrevSearchItem): void { + if (prevSearchItem) { + // delete only this item with the name + const i: number = this.prevSearch.indexOf(prevSearchItem); + this.prevSearch.splice(i, 1); + localStorage.setItem('prevSearch', JSON.stringify(this.prevSearch)); + } else { + // delete the whole "previous search" array + localStorage.removeItem('prevSearch'); } - /** - * remove one search item from the search history - * @params prevSearchItem - */ - resetPrevSearch(prevSearchItem?: PrevSearchItem): void { - if (prevSearchItem) { - // delete only this item with the name - const i: number = this.prevSearch.indexOf(prevSearchItem); - this.prevSearch.splice(i, 1); - localStorage.setItem('prevSearch', JSON.stringify(this.prevSearch)); - } else { - // delete the whole "previous search" array - localStorage.removeItem('prevSearch'); - } - - if (localStorage.getItem('prevSearch') === null) { - this.prevSearch = []; - } + if (localStorage.getItem('prevSearch') === null) { + this.prevSearch = []; } - - /** - * change the focus on the search input field - */ - changeFocus() { - this.selectProject.closeMenu(); - this.searchInput.nativeElement.focus(); - this.setFocus(); + } + + /** + * change the focus on the search input field + */ + changeFocus() { + this.selectProject.closeMenu(); + this.searchInput.nativeElement.focus(); + this.setFocus(); + } + + emitSearchParams() { + const searchParams: SearchParams = { + query: this.searchQuery, + mode: 'fulltext' + }; + + if (this.projectIri !== undefined) { + searchParams.filter = { + limitToProject: this.projectIri + }; } - emitSearchParams() { - const searchParams: SearchParams = { - query: this.searchQuery, - mode: 'fulltext', - }; - - if (this.projectIri !== undefined) { - searchParams.filter = { - limitToProject: this.projectIri, - }; - } - - this.search.emit(searchParams); - } + this.search.emit(searchParams); + } - togglePhonePanel() { - this.displayPhonePanel = !this.displayPhonePanel; - } + togglePhonePanel() { + this.displayPhonePanel = !this.displayPhonePanel; + } } diff --git a/libs/vre/advanced-search/src/lib/data-access/advanced-search-service/advanced-search.service.ts b/libs/vre/advanced-search/src/lib/data-access/advanced-search-service/advanced-search.service.ts index 4c6588ad46..0ab5526cd0 100644 --- a/libs/vre/advanced-search/src/lib/data-access/advanced-search-service/advanced-search.service.ts +++ b/libs/vre/advanced-search/src/lib/data-access/advanced-search-service/advanced-search.service.ts @@ -6,17 +6,17 @@ import { KnoraApiConnection, ListNodeV2, OntologiesMetadata, - OntologyMetadata, ReadOntology, ReadResource, ReadResourceSequence, ResourceClassAndPropertyDefinitions, ResourceClassDefinition, - ResourcePropertyDefinition, + ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; -import { Observable, Subject, of } from 'rxjs'; +import { Observable, of, Subject } from 'rxjs'; import { catchError, map, switchMap, takeUntil } from 'rxjs/operators'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; +import { OntologyV2ApiService } from '@dasch-swiss/vre/shared/app-api'; export interface ApiData { iri: string; @@ -43,11 +43,11 @@ export const ResourceLabel = export const ResourceLabelObject = { iri: 'resourceLabel', label: 'Resource Label', - objectType: ResourceLabel, + objectType: ResourceLabel }; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) export class AdvancedSearchService { // subjects to handle canceling of previous search requests when searching for a linked resource @@ -56,31 +56,21 @@ export class AdvancedSearchService { constructor( @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection - ) {} + private _dspApiConnection: KnoraApiConnection, + private _ontologyV2Api: OntologyV2ApiService + ) { + } // API call to get the list of ontologies allOntologiesList = (): Observable => { - return this._dspApiConnection.v2.onto.getOntologiesMetadata().pipe( - map((response: OntologiesMetadata | ApiResponseError) => { - if (response instanceof ApiResponseError) { - throw response; // caught by catchError operator - } - return response.ontologies - .filter( - (onto: OntologyMetadata) => - onto.attachedToProject !== - Constants.SystemProjectIRI - ) - .map((onto: { id: string; label: string }) => { - return { iri: onto.id, label: onto.label }; - }); - }), - catchError((err) => { - this._handleError(err); - return []; // return an empty array on error - }) - ); + return this._ontologyV2Api.getMetadata() + .pipe( + map((response) => response['@graph'] + .filter((onto) => onto['knora-api:attachedToProject'] !== Constants.SystemProjectIRI + ).map((onto) => { + return { iri: onto['@id'], label: onto['rdfs:label'] }; + }) + )); }; // API call to get the list of ontologies within the specified project iri @@ -216,7 +206,7 @@ export class AdvancedSearchService { label: label, objectType: objectType, isLinkedResourceProperty: linkProperty, - listIri: listNodeIri, + listIri: listNodeIri }; } else { console.error( @@ -230,7 +220,7 @@ export class AdvancedSearchService { iri: propDef.id, label: label, objectType: objectType, - isLinkedResourceProperty: linkProperty, + isLinkedResourceProperty: linkProperty }; }); }), @@ -290,7 +280,7 @@ export class AdvancedSearchService { label: label, objectType: objectType, isLinkedResourceProperty: linkProperty, - listIri: listNodeIri, + listIri: listNodeIri }; } else { console.error( @@ -304,7 +294,7 @@ export class AdvancedSearchService { iri: propDef.id, label: label, objectType: objectType, - isLinkedResourceProperty: linkProperty, + isLinkedResourceProperty: linkProperty }; }); }), @@ -326,7 +316,7 @@ export class AdvancedSearchService { return this._dspApiConnection.v2.search .doSearchByLabelCountQuery(searchValue, { - limitToResourceClass: resourceClassIri, + limitToResourceClass: resourceClassIri }) .pipe( takeUntil(this.cancelPreviousCountRequest$), // Cancel previous request @@ -355,7 +345,7 @@ export class AdvancedSearchService { return this._dspApiConnection.v2.search .doSearchByLabel(searchValue, offset, { - limitToResourceClass: resourceClassIri, + limitToResourceClass: resourceClassIri }) .pipe( takeUntil(this.cancelPreviousSearchRequest$), // Cancel previous request @@ -367,7 +357,7 @@ export class AdvancedSearchService { return of( response.resources.map((res: ReadResource) => ({ iri: res.id, - label: res.label, + label: res.label })) ); } diff --git a/libs/vre/advanced-search/src/lib/data-access/advanced-search-store/advanced-search-store.service.ts b/libs/vre/advanced-search/src/lib/data-access/advanced-search-store/advanced-search-store.service.ts index 190274cb09..afad7565be 100644 --- a/libs/vre/advanced-search/src/lib/data-access/advanced-search-store/advanced-search-store.service.ts +++ b/libs/vre/advanced-search/src/lib/data-access/advanced-search-store/advanced-search-store.service.ts @@ -390,8 +390,8 @@ export class AdvancedSearchStoreService extends ComponentStore values.includes(objectType)) - .map(([key, _]) => key); + .filter((v => v[1].includes(objectType))) + .map(([key]) => key); // if there are no matching operators in the map it means the property is a linked resource // i.e. http://0.0.0.0:3333/ontology/0801/newton/v2#letter @@ -488,8 +488,8 @@ export class AdvancedSearchStoreService extends ComponentStore values.includes(objectType)) - .map(([key, _]) => key); + .filter((v) => v[1].includes(objectType)) + .map(([key]) => key); // if there are no matching operators in the map it means the property is a linked resource // i.e. http://0.0.0.0:3333/ontology/0801/newton/v2#letter diff --git a/libs/vre/shared/app-api/.eslintrc.json b/libs/vre/shared/app-api/.eslintrc.json new file mode 100644 index 0000000000..6ebf70478b --- /dev/null +++ b/libs/vre/shared/app-api/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../../../.eslintrc-angular.json"] +} diff --git a/libs/vre/shared/app-api/README.md b/libs/vre/shared/app-api/README.md new file mode 100644 index 0000000000..6e3b0283db --- /dev/null +++ b/libs/vre/shared/app-api/README.md @@ -0,0 +1,7 @@ +# vre-shared-app-api + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test vre-shared-app-api` to execute the unit tests. diff --git a/libs/vre/shared/app-api/jest.config.ts b/libs/vre/shared/app-api/jest.config.ts new file mode 100644 index 0000000000..7b66c636b8 --- /dev/null +++ b/libs/vre/shared/app-api/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'vre-shared-app-api', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/vre/shared/app-api', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + 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-api/project.json b/libs/vre/shared/app-api/project.json new file mode 100644 index 0000000000..35156bdf9c --- /dev/null +++ b/libs/vre/shared/app-api/project.json @@ -0,0 +1,34 @@ +{ + "name": "vre-shared-app-api", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/vre/shared/app-api/src", + "prefix": "dasch-swiss", + "tags": [], + "projectType": "library", + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/vre/shared/app-api/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-api/**/*.ts", + "libs/vre/shared/app-api/**/*.html" + ] + } + } + } +} diff --git a/libs/vre/shared/app-api/src/index.ts b/libs/vre/shared/app-api/src/index.ts new file mode 100644 index 0000000000..ab07449acd --- /dev/null +++ b/libs/vre/shared/app-api/src/index.ts @@ -0,0 +1,12 @@ +export * from './lib/services/admin/administrative-permission-api.service'; +export * from './lib/services/admin/group-api.service'; +export * from './lib/services/admin/list-api.service'; +export * from './lib/services/admin/permission-api.service'; +export * from './lib/services/admin/project-api.service'; +export * from './lib/services/admin/user-api.service'; + +export * from './lib/services/health-api.service'; +export * from './lib/services/version-api.service'; + +export * from './lib/services/v2/ontology/ontology-v2-api.service'; +export * from './lib/services/v2/list/list-v2-api.service'; diff --git a/libs/vre/shared/app-api/src/lib/interfaces/graph.interface.ts b/libs/vre/shared/app-api/src/lib/interfaces/graph.interface.ts new file mode 100644 index 0000000000..a6183bd60b --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/interfaces/graph.interface.ts @@ -0,0 +1,3 @@ +export interface Graph { + '@graph': T[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/admin/administrative-permission-api.service.ts b/libs/vre/shared/app-api/src/lib/services/admin/administrative-permission-api.service.ts new file mode 100644 index 0000000000..7650a62dd8 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/admin/administrative-permission-api.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { AdministrativePermissionsResponse, CreateAdministrativePermission } from '@dasch-swiss/dsp-js'; +import { BaseApi } from '../base-api'; + +@Injectable({ providedIn: 'root' }) +export class AdministrativePermissionApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('admin/permissions/ap'); + } + + list(projectIri: string) { + return this._http.get(`${this.baseUri}/${encodeURIComponent(projectIri)}`); + } + + get(projectIri: string, groupIri: string) { + return this._http.get(`${this._permissionRoute(projectIri)}/${encodeURIComponent(groupIri)}`); + } + + create(permission: CreateAdministrativePermission) { + if (!permission.forGroup || !permission.forProject) { + throw new Error('Group and project are required when creating a new administrative permission.'); + } + + if (permission.hasPermissions.length === 0) { + throw new Error('At least one permission is expected.'); + } + + return this._http.post(this.baseUri, permission); + } + + + private _permissionRoute(projectIri: string) { + return `${this.baseUri}/${encodeURIComponent(projectIri)}`; + } + + +} diff --git a/libs/vre/shared/app-api/src/lib/services/admin/group-api.service.ts b/libs/vre/shared/app-api/src/lib/services/admin/group-api.service.ts new file mode 100644 index 0000000000..8c4a5e48ec --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/admin/group-api.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BaseApi } from '../base-api'; +import { CreateGroupRequest, GroupsResponse, UpdateGroupRequest } from '@dasch-swiss/dsp-js'; + +@Injectable({ + providedIn: 'root' +}) +export class GroupApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('admin/groups'); + } + + list() { + return this._http.get(this.baseUri); + } + + get(iri: string) { + return this._http.get(this._groupRoute(iri)); + } + + create(group: CreateGroupRequest) { + return this._http.post(this.baseUri, group); + } + + update(iri: string, updatedGroup: UpdateGroupRequest) { + return this._http.put(this._groupRoute(iri), updatedGroup); + } + + updateStatus(iri: string, status: boolean) { + return this._http.put(`${this._groupRoute(iri)}/status`, { status }); + } + + delete(iri: string) { + return this._http.delete(this._groupRoute(iri)); + } + + private _groupRoute(iri: string) { + return `${this.baseUri}/${encodeURIComponent(iri)}`; + } +} 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 new file mode 100644 index 0000000000..e5b4b44e3f --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/admin/list-api.service.ts @@ -0,0 +1,168 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BaseApi } from '../base-api'; +import { + ChildNodeInfoResponse, + CreateChildNodeRequest, + CreateListRequest, + DeleteChildNodeCommentsResponse, + DeleteListNodeResponse, + DeleteListResponse, + ListChildNodeResponse, + ListInfoResponse, + ListNodeInfoResponse, + ListResponse, + ListsResponse, + RepositionChildNodeRequest, + RepositionChildNodeResponse, + UpdateChildNodeCommentsRequest, + UpdateChildNodeLabelsRequest, + UpdateChildNodeNameRequest, + UpdateChildNodeRequest, + UpdateGroupRequest, + UpdateListInfoRequest, +} from '@dasch-swiss/dsp-js'; + +@Injectable({ + providedIn: 'root', +}) +export class ListApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('admin/lists'); + } + + list() { + return this._http.get(this.baseUri); + } + + get(iri: string) { + return this._http.get( + this._listRoute(iri) + ); + } + + create(list: CreateListRequest) { + return this._http.post(this.baseUri, list); + } + + update(iri: string, updatedGroup: UpdateGroupRequest) { + return this._http.put( + this._listRoute(iri), + updatedGroup + ); + } + + delete(iri: string) { + return this._http.delete( + this._listRoute(iri) + ); + } + + listInProject(projectIri: string) { + return this._http.get( + `${this.baseUri}?projectIri=${encodeURIComponent(projectIri)}` + ); + } + + getInfo(iri: string) { + return this._http.get( + `${this.baseUri}/infos/${encodeURIComponent(iri)}` + ); + } + + getNodeInfo(iri: string) { + return this._http.get( + `${this._listRoute(iri)}/info}` + ); + } + + updateInfo(iri: string, updatedList: UpdateListInfoRequest) { + return this._http.put( + this._listRoute(iri), + updatedList + ); + } + + updateChildName( + iri: string, + updatedChildNodeName: UpdateChildNodeNameRequest + ) { + return this._http.put( + `${this._listRoute(iri)}/name`, + updatedChildNodeName + ); + } + + updateChildLabels(iri: string, labels: UpdateChildNodeLabelsRequest) { + return this._http.put( + `${this._listRoute(iri)}/labels`, + labels + ); + } + + updateChildComments(iri: string, comments: UpdateChildNodeCommentsRequest) { + return this._http.put( + `${this._listRoute(iri)}/comments`, + comments + ); + } + + deleteChildComments(iri: string) { + // TODO route should rather be /lists/ID/comments + return this._http.delete( + `${this.baseUri}/comments/${encodeURIComponent(iri)}` + ); + } + + createChildNode(parentNodeIri: string, node: CreateChildNodeRequest) { + return this._http.post( + this._listRoute(parentNodeIri), + node + ); + } + + updateChildNodePosition( + iri: string, + repositionRequest: RepositionChildNodeRequest + ) { + return this._http.put( + `${this._listRoute(iri)}/position`, + repositionRequest + ); + } + + 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 + ); + } + + repositionChildNode( + iri: string, + repositionRequest: RepositionChildNodeRequest + ) { + return this._http.put( + `${this._listRoute(iri)}/position`, + repositionRequest + ); + } + + deleteListNode(iri: string) { + return this._http.delete(this._listRoute(iri)); + } + + private _listRoute(iri: string) { + return `${this.baseUri}/${encodeURIComponent(iri)}`; + } +} diff --git a/libs/vre/shared/app-api/src/lib/services/admin/permission-api.service.ts b/libs/vre/shared/app-api/src/lib/services/admin/permission-api.service.ts new file mode 100644 index 0000000000..9572143c16 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/admin/permission-api.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from '@angular/core'; +import { BaseApi } from '../base-api'; +import { HttpClient } from '@angular/common/http'; +import { + AdministrativePermissionResponse, + CreateDefaultObjectAccessPermission, + DefaultObjectAccessPermissionResponse, + DefaultObjectAccessPermissionsResponse, + ProjectPermissionsResponse, + UpdateAdministrativePermission, + UpdateAdministrativePermissionGroup, + UpdateDefaultObjectAccessPermission, UpdateDefaultObjectAccessPermissionProperty, + UpdateDefaultObjectAccessPermissionResourceClass +} from '@dasch-swiss/dsp-js'; + +@Injectable({ providedIn: 'root' }) +export class PermissionApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('admin/permissions'); + } + + getProjectPermissions(projectIri: string) { + return this._http.get(`${this.baseUri}/${encodeURIComponent(projectIri)}`); + } + + updateAdministrativePermission(permissionIri: string, permisssion: UpdateAdministrativePermission) { + // TODO from name, it should be in admin-permission-api.service.ts, but from route it is in the right place. + return this._http.put(`${this.baseUri}/${encodeURIComponent(permissionIri)}/hasPermissions`, permisssion); + } + + updateAdministrativePermissionGroup(permissionIri: string, updatedGroup: UpdateAdministrativePermissionGroup) { + // TODO from name, it should be in admin-permission-api.service.ts, but from route it is in the right place. + return this._http.put(`${this.baseUri}/${encodeURIComponent(permissionIri)}/group`, updatedGroup); + } + + getDefaultObjectAccessPermissions(projectIri: string) { + return this._http.get(`${this.baseUri}/doap/${encodeURIComponent(projectIri)}`); + } + + createDefaultObjectAccessPermission(defaultObject: CreateDefaultObjectAccessPermission) { + // A default object access permission must + // always reference a project + if (!defaultObject.forProject) { + throw new Error('Project is required when creating a new default object access permission.'); + } + + if (defaultObject.hasPermissions.length === 0) { + throw new Error('At least one permission is expected.'); + } + + /* + A default object access permission can only reference either a group, a resource class, a property, + or a combination of resource class and property. + */ + if (((defaultObject.forGroup && !defaultObject.forResourceClass && (!defaultObject.forProperty + || !defaultObject.forGroup) && defaultObject.forResourceClass && (!defaultObject.forProperty + || !defaultObject.forGroup) && !defaultObject.forResourceClass && defaultObject.forProperty) + || !defaultObject.forGroup )&& defaultObject.forResourceClass && defaultObject.forProperty + ) { + return this._http.post(`${this.baseUri}/doap`, defaultObject); + } else { + throw new Error('Invalid combination of properties for creation of new default object access permission.'); + } + } + + updateDefaultObjectAccessPermission(permissionIri: string, permission: UpdateDefaultObjectAccessPermission) { + // TODO route should be replaced by /doap + return this._http.put(`${this.baseUri}/${encodeURIComponent(permissionIri)}/hasPermissions`,permission); + } + + updateDefaultObjectAccessPermissionGroup(permissionIri: string, permission: UpdateDefaultObjectAccessPermission) { + // TODO route should be replaced by /doap + return this._http.put(`${this.baseUri}/${encodeURIComponent(permissionIri)}/group`,permission); + } + + updateDefaultObjectAccessPermissionResourceClass(permissionIri: string, permission: UpdateDefaultObjectAccessPermissionResourceClass) { + // TODO route should be replaced by /doap + return this._http.put(`${this.baseUri}/${encodeURIComponent(permissionIri)}/resourceClass`,permission); + } + + updateDefaultObjectAccessPermissionProperty(permissionIri: string, permission: UpdateDefaultObjectAccessPermissionProperty) { + // TODO route should be replaced by /doap + return this._http.put(`${this.baseUri}/${encodeURIComponent(permissionIri)}/property`,permission); + } + + delete(permissionIri: string) { + return this._http.delete(this._permissionRoute(permissionIri)); + } + + private _permissionRoute(permissionIri: string) { + return `${this.baseUri}/${encodeURIComponent(permissionIri)}`; + } + + +} diff --git a/libs/vre/shared/app-api/src/lib/services/admin/project-api.service.ts b/libs/vre/shared/app-api/src/lib/services/admin/project-api.service.ts new file mode 100644 index 0000000000..32b6fc8981 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/admin/project-api.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { + KeywordsResponse, MembersResponse, + Project, + ProjectResponse, ProjectRestrictedViewSettingsResponse, + ProjectsResponse, + UpdateProjectRequest +} from '@dasch-swiss/dsp-js'; +import { BaseApi } from '../base-api'; + +export type ProjectIdentifier = 'iri' | 'shortname' | 'shortcode'; +@Injectable({ + providedIn: 'root' +}) +export class ProjectApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('admin/projects'); + } + + list() { + return this._http.get(this.baseUri); + } + + get(id: string, idType: ProjectIdentifier = 'iri') { + return this._http.get(this._projectRoute(id, idType)); + } + + create(project: Project) { + return this._http.post(this.baseUri, project); + } + + update(iri: string, updatedProject: UpdateProjectRequest) { + return this._http.put(this._projectRoute(iri), updatedProject); + } + + delete(iri: string) { + return this._http.delete(this._projectRoute(iri)); + } + + getKeywordsForProject(iri: string) { + return this._http.get(`${this._projectRoute(iri)}/Keywords`); + } + + getMembersForProject(id: string, idType: ProjectIdentifier = 'iri') { + return this._http.get(`${this._projectRoute(id, idType)}/members`); + } + + getAdminMembersForProject(id: string, idType: ProjectIdentifier = 'iri') { + return this._http.get(`${this._projectRoute(id, idType)}/admin-members`); + } + + getRestrictedViewSettingsForProject(id: string, idType: ProjectIdentifier = 'iri') { + return this._http.get(`${this._projectRoute(id, idType)}/RestrictedViewSettings`); + } + + private _projectRoute(id: string, idType: ProjectIdentifier = 'iri') { + return `${this.baseUri}/${idType}/${encodeURIComponent(id)}`; + } +} + diff --git a/libs/vre/shared/app-api/src/lib/services/admin/user-api.service.ts b/libs/vre/shared/app-api/src/lib/services/admin/user-api.service.ts new file mode 100644 index 0000000000..a943d56ca3 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/admin/user-api.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { + GroupsResponse, + ProjectsResponse, + UpdateUserRequest, + User, + UserResponse, + UsersResponse +} from '@dasch-swiss/dsp-js'; +import { BaseApi } from '../base-api'; + +export type UserIdentifier = 'iri' | 'email' | 'username'; + + +@Injectable({ + providedIn: 'root' +}) +export class UserApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('admin/users'); + } + + list() { + return this._http.get(this.baseUri); + } + + get(id: string, idType: UserIdentifier = 'iri') { + return this._http.get(this._userRoute(id, idType)); + } + + create(user: User) { + return this._http.post(this.baseUri, user); + } + + delete(id: string) { + return this._http.delete(this._userRoute(id)); + } + + getGroupMembershipsForUser(iri: string) { + return this._http.get(`${this._userRoute(iri)}/group-memberships`); + } + + getProjectMembershipsForUser(iri: string) { + return this._http.get(`${this._userRoute(iri)}/project-memberships`); + } + + getProjectAdminMembershipsForUser(iri: string) { + return this._http.get(`${this._userRoute(iri)}/project-admin-memberships`); + } + + updateBasicInformation(iri: string, updatedUser: UpdateUserRequest) { + return this._http.put(`${this._userRoute(iri)}/BasicUserInformation`, updatedUser); + } + + updateStatus(iri: string, status: boolean) { + return this._http.put(`${this._userRoute(iri)}/Status`, { status }); + } + + updatePassword(iri: string, currentPassword: boolean, newPassword: boolean) { + return this._http.put(`${this._userRoute(iri)}/Password`, { + requesterPassword: currentPassword, + newPassword + }); + } + + + addToGroupMembership(userIri: string, groupIri: string) { + return this._http.post(`${this._userRoute(userIri)}/group-memberships/${encodeURIComponent(groupIri)}`, {}); + } + + removeFromGroupMembership(userIri: string, groupIri: string) { + return this._http.post(`${this._userRoute(userIri)}/group-memberships/${encodeURIComponent(groupIri)}`, {}); + } + + addToProjectMembership(userIri: string, projectIri: string, adminProject = false) { + return this._http.post(`${this._userRoute(userIri)}/project-${adminProject ? 'admin-' : ''}memberships/${encodeURIComponent(projectIri)}`, {}); + } + + removeFromProjectMembership(userIri: string, projectIri: string, adminProject = false) { + return this._http.post(`${this._userRoute(userIri)}/project-${adminProject ? 'admin-' : ''}memberships/${encodeURIComponent(projectIri)}`, {}); + } + + updateSystemAdminMembership(iri: string, isSystemAdmin: boolean) { + return this._http.put(`${this._userRoute(iri)}/SystemAdmin`, { systemAdmin: isSystemAdmin }); + } + + private _userRoute(id: string, idType: UserIdentifier = 'iri') { + return `${this.baseUri}/${idType}/${encodeURIComponent(id)}`; + } +} diff --git a/libs/vre/shared/app-api/src/lib/services/base-api.ts b/libs/vre/shared/app-api/src/lib/services/base-api.ts new file mode 100644 index 0000000000..811ee96ac9 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/base-api.ts @@ -0,0 +1,7 @@ +export abstract class BaseApi { + protected baseUri: string; + constructor(endpoint: string) { + const host = 'http://0.0.0.0:3333'; + this.baseUri = `${host}/${endpoint}`; + } +} diff --git a/libs/vre/shared/app-api/src/lib/services/health-api.service.ts b/libs/vre/shared/app-api/src/lib/services/health-api.service.ts new file mode 100644 index 0000000000..0dbcf4ee01 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/health-api.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BaseApi } from './base-api'; +import { HealthResponse } from '@dasch-swiss/dsp-js'; + +@Injectable({ + providedIn: 'root' +}) +export class HealthApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('health'); + } + + get() { + return this._http.get(this.baseUri); + } +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/authentication-api.service.ts b/libs/vre/shared/app-api/src/lib/services/v2/authentication-api.service.ts new file mode 100644 index 0000000000..d506e42c3c --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/authentication-api.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BaseApi } from '../base-api'; +import { CredentialsResponse, LoginResponse } from '@dasch-swiss/dsp-js'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('v2/authentication'); + } + + checkCredentials() { + return this._http.get(this.baseUri); + } + + login(property: 'iri' | 'email' | 'username', id: string, password: string) { + const credentials: any = { + password + }; + credentials[property] = id; + + return this._http.post(this.baseUri, credentials); + } + + logout() { + return this._http.delete(this.baseUri); + } +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/list/list-node.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/list/list-node.interface.ts new file mode 100644 index 0000000000..457c3456de --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/list/list-node.interface.ts @@ -0,0 +1,6 @@ +export interface ListNode { + '@id': string; + 'rdfs:label': string; + 'knora-api:isRootNode': string; + 'knora-api:hasSubListNode': ListNode[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/list/list-v2-api.service.ts b/libs/vre/shared/app-api/src/lib/services/v2/list/list-v2-api.service.ts new file mode 100644 index 0000000000..ba1232396d --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/list/list-v2-api.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BaseApi } from '../../base-api'; +import { ListNode } from './list-node.interface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ListV2ApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('v2'); // TODO weird + } + + getNode(nodeIri: string) { + return this._http.get(`${this.baseUri}/node/${encodeURIComponent(nodeIri)}`); + } + + getListWithInterface(nodeIri: string) { + return this._http.get(`${this.baseUri}/lists/${encodeURIComponent(nodeIri)}`); + } +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/can-do-response.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/can-do-response.interface.ts new file mode 100644 index 0000000000..b204b85319 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/can-do-response.interface.ts @@ -0,0 +1,4 @@ +export interface CanDoResponse { + canDo: boolean; + cannotDoReason?: string; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-ontology.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-ontology.interface.ts new file mode 100644 index 0000000000..08a4de0c85 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-ontology.interface.ts @@ -0,0 +1,6 @@ +export interface CreateOntology { + 'comment'?: string; + 'rdfs:label': string; + 'knora-api:attachedToProject'?: string; + 'name'?: string; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class-payload.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class-payload.interface.ts new file mode 100644 index 0000000000..16f1ff5ce1 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class-payload.interface.ts @@ -0,0 +1,5 @@ +import { CreateResourceClass } from './create-resource-class.interface'; + +export interface CreateResourceClassPayload extends CreateResourceClass { + '@id': string; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class.interface.ts new file mode 100644 index 0000000000..7f2fe8c20b --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-class.interface.ts @@ -0,0 +1,7 @@ +export interface CreateResourceClass { + name: string; + '@type': string; + 'rdfs:label': string; + 'comment'?: string; + 'rdfs:subClassOf': string[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-property.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-property.interface.ts new file mode 100644 index 0000000000..7f01f7034b --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/create-resource-property.interface.ts @@ -0,0 +1,12 @@ +import { StringLiteralV2 } from './string-literal.v2'; + +export interface CreateResourceProperty { + 'name': string; + 'knora-api:subjectType': string; + 'knora-api:objectType': string; + 'comment'?: string; + 'rdfs:subPropertyOf'?: string; + 'guiElement'?: string; + 'guiAttributes'?: string; + 'rdfs:label': StringLiteralV2[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/i-has-property.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/i-has-property.interface.ts new file mode 100644 index 0000000000..5a235e292b --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/i-has-property.interface.ts @@ -0,0 +1,28 @@ +export enum Cardinality { + /** + * Cardinality 1 (required). + */ + "_1" = 0, + + /** + * Cardinality 0-1 (optional). + */ + "_0_1" = 1, + + /** + * Cardinality 0-n (may have many) + */ + "_0_n" = 2, + + /** + * Cardinality 1-n (at least one). + */ + "_1_n" = 3 +} + +export interface IHasProperty { + propertyIndex: string; + cardinality: Cardinality; + guiOrder?: number; + isInherited?: boolean; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-metadata.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-metadata.interface.ts new file mode 100644 index 0000000000..7280c6c945 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-metadata.interface.ts @@ -0,0 +1,8 @@ +export interface OntologyMetadata { + '@id': string; + '@type': string; + 'rdfs:label': string; + 'comment'?: string; + 'knora-api:lastModificationDate'?: string; + 'knora-api:attachedToProject'?: string; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-v2-api.service.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-v2-api.service.ts new file mode 100644 index 0000000000..cf352dfdd3 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/ontology-v2-api.service.ts @@ -0,0 +1,140 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BaseApi } from '../../base-api'; +import { OntologyMetadata } from './ontology-metadata.interface'; +import { Graph } from '../../../interfaces/graph.interface'; +import { ReadOntology } from './read-ontology.interface'; +import { CreateOntology } from './create-ontology.interface'; +import { CreateResourceClassPayload } from './create-resource-class-payload.interface'; +import { ResourceClassDefinitionWithAllLanguages } from './resource-class-definition-with-all-languages.interface'; +import { UpdateResourceClass } from './update-resource-class.interface'; +import { UpdateResourceProperty } from './update-resource-property.interface'; +import { UpdateResourcePropertyResponse } from './update-resource-property-response.interface'; +import { CreateResourceProperty } from './create-resource-property.interface'; +import { + ResourcePropertyDefinitionWithAllLanguages +} from './resource-property-definition-with-all-languages.interface'; +import { UpdateResourcePropertyGuiElement } from './update-resource-property-gui-element.interface'; +import { CanDoResponse } from './can-do-response.interface'; +import { Cardinality, IHasProperty } from './i-has-property.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class OntologyV2ApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('v2/ontologies'); + } + + getMetadata() { + return this._http.get>(`${this.baseUri}/metadata`); + } + + get(ontologyIri: string, allLanguages = false) { + return this._http.get(`${this.baseUri}/allentities/${encodeURIComponent(ontologyIri)}${allLanguages ? '?allLanguages=true' : ''}`); + } + + getByProject(iri: string) { + return this._http.get>(`${this.baseUri}/metadata/${encodeURIComponent(iri)}`); + } + + create(ontology: CreateOntology) { + return this._http.post(this.baseUri, ontology); + } + + canDelete(iri: string) { + return this._http.get(`${this.baseUri}/candeleteontology/${encodeURIComponent(iri)}`); + } + + delete(iri: string, lastModificationDate: string) { + return this._http.get(`${this.baseUri}/${encodeURIComponent(iri)}?lastModificationDate=${lastModificationDate}`); + } + + update() { + // TODO weird + } + + deleteComment(iri: string, lastModificationDate: string) { + return this._http.delete(`${this.baseUri}/comment/${encodeURIComponent(iri)}?lastModificationDate=${lastModificationDate}`); + } + + createResourceClass(resourceClass: CreateResourceClassPayload) { + return this._http.post(`${this.baseUri}/classes`, resourceClass); + } + + updateResourceClass(resourceClass: UpdateResourceClass) { + return this._http.put(`${this.baseUri}/classes`, resourceClass); + } + + updateResourceProperty(updateProperty: UpdateResourceProperty) { + return this._http.put(`${this.baseUri}/properties`, updateProperty); + } + + canDeleteResourceClass(iri: string) { + return this._http.get(`${this.baseUri}/candeleteclass/${iri}`); + } + + deleteResourceClass(iri: string, lastModificationDate: string) { + return this._http.delete(`${this.baseUri}/classes/${encodeURIComponent(iri)}?lastModificationDate=${lastModificationDate}`); + } + + deleteResourceClassComment(iri: string, lastModificationDate: string) { + return this._http.delete(`${this.baseUri}/classes/comment/${encodeURIComponent(iri)}?lastModificationDate=${lastModificationDate}`); + } + + createResourceProperty(resource: CreateResourceProperty) { + return this._http.post(`${this.baseUri}/properties`, resource); + } + + replaceGuiElementOfProperty(payload: UpdateResourcePropertyGuiElement) { + return this._http.put(`${this.baseUri}/properties/guielement`, payload); + } + + canDeleteResourceProperty(iri: string) { + return this._http.get(`${this.baseUri}/candeleteproperty/${encodeURIComponent(iri)}`); + } + + deleteResourceProperty(iri: string, lastModificationDate: string) { + return this._http.delete(`${this.baseUri}/properties/${encodeURIComponent(iri)}?lastModificationDate=${lastModificationDate}`); + } + + deleteResourcePropertyComment(iri: string, lastModificationDate: string) { + return this._http.delete(`${this.baseUri}/properties/comment/${encodeURIComponent(iri)}?lastModificationDate=${lastModificationDate}`); + } + + addCardinalityToResourceClass(payload: any) { + return this._http.post(`${this.baseUri}/cardinalities`, payload); + } + + canReplaceCardinalityOfResourceClass(iri: string) { + return this._http.get(`${this.baseUri}/canreplacecardinalities/${encodeURIComponent(iri)}`); + } + + canReplaceCardinalityOfResourceClassWith(resourceClassIri: string, propertyIri: string, newCardinality: string) { + return this._http.get(`${this.baseUri}/canreplacecardinalities/${encodeURIComponent(resourceClassIri)}?propertyIri=${encodeURIComponent(propertyIri)}&newCardinality=${newCardinality}`); + } + + replaceCardinalityOfResourceClass(cardinalities: Graph<{ cardinalities: IHasProperty[] }>) { + return this._http.put(`${this.baseUri}/cardinalities`, cardinalities); + } + + canDeleteCardinalityFromResourceClass(cardinalities: Graph<{ cardinalities: Cardinality[] }>) { + return this._http.post(`${this.baseUri}/candeletecardinalities`, cardinalities); + } + + deleteCardinalityFromResourceClass(payload: Graph<{ cardinalities: Cardinality[] }>) { + return this._http.patch(`${this.baseUri}/cardinalities`, payload); + } + + replaceGuiOrderOfCardinalities(payload) { + return this._http.put(`${this.baseUri}/guiorder`, payload); + } + + private _updateMetadata(ontologyMetadata: OntologyMetadata) { + return this._http.put(`${this.baseUri} / metadata`, ontologyMetadata); + } + + private _update(iri: string, ontologyMetaData: OntologyMetadata) { + return this._http.put(`${this.baseUri} /${encodeURIComponent(iri)}`, ontologyMetaData); + } +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/read-ontology.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/read-ontology.interface.ts new file mode 100644 index 0000000000..ce11ccc25d --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/read-ontology.interface.ts @@ -0,0 +1,6 @@ +export interface ReadOntology { + '@id': string; + 'rdfs:label': string; + 'comment'?: string; + 'knora-api:lastModificationDate'?: string; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-class-definition-with-all-languages.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-class-definition-with-all-languages.interface.ts new file mode 100644 index 0000000000..509dae68a9 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-class-definition-with-all-languages.interface.ts @@ -0,0 +1,6 @@ +import { StringLiteralV2 } from './string-literal.v2'; + +export interface ResourceClassDefinitionWithAllLanguages { + comment?: string; + comments: StringLiteralV2[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-property-definition-with-all-languages.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-property-definition-with-all-languages.interface.ts new file mode 100644 index 0000000000..38aa3f19b8 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/resource-property-definition-with-all-languages.interface.ts @@ -0,0 +1,13 @@ +import { StringLiteralV2 } from './string-literal.v2'; + +export interface ResourcePropertyDefinitionWithAllLanguages { + '@id': string; + 'knora-api:subjectType': string; + 'knora-api:objectType': string; + 'knora-api:isLinkProperty': boolean; + 'comment'?: string; + 'rdfs:subPropertyOf'?: string; + 'guiElement'?: string; + 'guiAttributes'?: string; + 'rdfs:label': StringLiteralV2[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/string-literal.v2.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/string-literal.v2.ts new file mode 100644 index 0000000000..537d089a73 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/string-literal.v2.ts @@ -0,0 +1,4 @@ +export interface StringLiteralV2 { + '@language'?: string; + '@value': string; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-class.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-class.interface.ts new file mode 100644 index 0000000000..b4c38b6330 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-class.interface.ts @@ -0,0 +1,7 @@ +import { StringLiteralV2 } from './string-literal.v2'; + +export interface UpdateResourceClass { + '@id': string; + '@type': string; + 'rdfs:labels': StringLiteralV2[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-gui-element.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-gui-element.interface.ts new file mode 100644 index 0000000000..73c30e3f07 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-gui-element.interface.ts @@ -0,0 +1,4 @@ +export interface UpdateResourcePropertyGuiElement { + 'knora-api:guiElement': string; + 'knora-api:guiAttributes': string[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-response.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-response.interface.ts new file mode 100644 index 0000000000..bfa11a5728 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property-response.interface.ts @@ -0,0 +1,6 @@ +import { StringLiteralV2 } from './string-literal.v2'; + +export interface UpdateResourcePropertyResponse { + '@id': string; + 'rdfs:labels': StringLiteralV2[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property.interface.ts b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property.interface.ts new file mode 100644 index 0000000000..75d2eb3e7e --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/v2/ontology/update-resource-property.interface.ts @@ -0,0 +1,7 @@ +import { StringLiteralV2 } from './string-literal.v2'; + +export interface UpdateResourceProperty { + '@id': string; + '@type': string; + 'rdfs:labels': StringLiteralV2[]; +} diff --git a/libs/vre/shared/app-api/src/lib/services/version-api.service.ts b/libs/vre/shared/app-api/src/lib/services/version-api.service.ts new file mode 100644 index 0000000000..dfac7bcd28 --- /dev/null +++ b/libs/vre/shared/app-api/src/lib/services/version-api.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BaseApi } from './base-api'; +import { VersionResponse } from '@dasch-swiss/dsp-js'; + +@Injectable({ + providedIn: 'root' +}) +export class VersionApiService extends BaseApi { + constructor(private _http: HttpClient) { + super('version'); + } + + get() { + return this._http.get(this.baseUri); + } +} diff --git a/libs/vre/shared/app-api/src/test-setup.ts b/libs/vre/shared/app-api/src/test-setup.ts new file mode 100644 index 0000000000..b3525effab --- /dev/null +++ b/libs/vre/shared/app-api/src/test-setup.ts @@ -0,0 +1,8 @@ +// @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-api/tsconfig.json b/libs/vre/shared/app-api/tsconfig.json new file mode 100644 index 0000000000..b9e5be0863 --- /dev/null +++ b/libs/vre/shared/app-api/tsconfig.json @@ -0,0 +1,29 @@ +{ + "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-api/tsconfig.lib.json b/libs/vre/shared/app-api/tsconfig.lib.json new file mode 100644 index 0000000000..9127387056 --- /dev/null +++ b/libs/vre/shared/app-api/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "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-api/tsconfig.spec.json b/libs/vre/shared/app-api/tsconfig.spec.json new file mode 100644 index 0000000000..6e5925e5c4 --- /dev/null +++ b/libs/vre/shared/app-api/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "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-error-handler/src/lib/app-error-handler.ts b/libs/vre/shared/app-error-handler/src/lib/app-error-handler.ts index 0ba489658a..ea1997b664 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 @@ -57,11 +57,6 @@ export class AppErrorHandler implements ErrorHandler { error.error && !(error.error instanceof AjaxError && error.error['response']); - const apiResponseMessage = - error.error instanceof AjaxError && error.error['response'] - ? error.error['response'].error - : undefined; - if ( ((error.status > 499 && error.status < 600) || apiServerError) && error.status !== 504 diff --git a/libs/vre/shared/app-helper-services/src/lib/project.service.ts b/libs/vre/shared/app-helper-services/src/lib/project.service.ts index 1849394c38..a1f0335fc9 100644 --- a/libs/vre/shared/app-helper-services/src/lib/project.service.ts +++ b/libs/vre/shared/app-helper-services/src/lib/project.service.ts @@ -72,7 +72,7 @@ export class ProjectService { const groupsPerProject = user.permissions.groupsPerProject ? user.permissions.groupsPerProject : {}; return ProjectService.IsProjectOrSysAdmin(groupsPerProject, userProjectGroups, projectIri); } - + static IsProjectOrSysAdmin(groupsPerProject: {[key: string]: string[]}, userProjectGroups: string[], projectIri: string): boolean { const isMemberOfSystemAdminGroup = ProjectService.IsMemberOfSystemAdminGroup(groupsPerProject); diff --git a/libs/vre/shared/app-session/src/lib/app-session.ts b/libs/vre/shared/app-session/src/lib/app-session.ts index 6adf37ef97..b12fec696d 100644 --- a/libs/vre/shared/app-session/src/lib/app-session.ts +++ b/libs/vre/shared/app-session/src/lib/app-session.ts @@ -13,6 +13,7 @@ import { Observable, of } from 'rxjs'; import { catchError, map, takeLast } from 'rxjs/operators'; import { Session } from './session'; import { toObservable } from '@angular/core/rxjs-interop'; +import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; @Injectable({ providedIn: 'root', @@ -31,7 +32,9 @@ export class SessionService { */ readonly MAX_SESSION_TIME: number = 3600; - constructor() { + constructor( + private _userApiService: UserApiService, + ) { // check if the (possibly) existing session is still valid and if not, destroy it this.isSessionValid() .pipe(takeLast(1)) @@ -46,14 +49,10 @@ export class SessionService { * set the application state for current/logged-in user */ const session = this.session() as Session; - this._dspApiConnection.admin.usersEndpoint - .getUserByUsername(session.user.name as string) + this._userApiService + .get(session.user.name as string, 'username') .subscribe( - ( - response: - | ApiResponseData - | ApiResponseError - ) => { + response => { if (response instanceof ApiResponseData) { this._applicationStateService.set( session.user.name, @@ -89,15 +88,11 @@ export class SessionService { this._dspApiConnection.v2.jsonWebToken = jwt ? jwt : ''; // get user information - return this._dspApiConnection.admin.usersEndpoint - .getUser(type, identifier) + return this._userApiService + .get(identifier, type) .pipe( map( - ( - response: - | ApiResponseData - | ApiResponseError - ) => { + response => { this._storeSessionInLocalStorage(response, jwt); // return type is void return true; @@ -139,7 +134,7 @@ export class SessionService { ); } ), - catchError(error => { + 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 @@ -182,18 +177,15 @@ export class SessionService { * @param jwt JSON web token string */ private _storeSessionInLocalStorage( - response: ApiResponseData | ApiResponseError, + response: UserResponse, jwt: string ) { - let session: Session; - - if (response instanceof ApiResponseData) { 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.body.user.permissions.groupsPerProject; + response.user.permissions.groupsPerProject; if (groupsPerProject) { const groupsPerProjectKeys: string[] = @@ -218,12 +210,12 @@ export class SessionService { } // store session information in browser's localstorage - session = { + const session = { id: this._setTimestamp(), user: { - name: response.body.user.username, + name: response.user.username, jwt: jwt, - lang: response.body.user.lang, + lang: response.user.lang, sysAdmin: sysAdmin, projectAdmin: projectAdmin, }, @@ -232,11 +224,6 @@ export class SessionService { // update localStorage localStorage.setItem('session', JSON.stringify(session)); this.session.set(session); - } else { - localStorage.removeItem('session'); - this.session.set(undefined); - // console.error(response); - } } /** 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 403250416a..acca1e8232 100644 --- a/libs/vre/shared/app-session/src/lib/auth.service.ts +++ b/libs/vre/shared/app-session/src/lib/auth.service.ts @@ -15,7 +15,6 @@ export class AuthService { private tokenRefreshIntervalId: any; private _isLoggedIn$ = new BehaviorSubject(this.isLoggedIn()); private _dspApiConnection = inject(DspApiConnectionToken); - private _errorHandler = inject(AppErrorHandler); isLoggedIn$ = this._isLoggedIn$.asObservable(); @@ -27,8 +26,7 @@ export class AuthService { constructor( private store: Store, - private router: Router, - // private intervalWrapper: IntervalWrapperService + private router: Router // private intervalWrapper: IntervalWrapperService ) { // check if the (possibly) existing session is still valid and if not, destroy it this.isSessionValid() @@ -69,7 +67,7 @@ export class AuthService { return this._updateSessionId(credentials); } ), - catchError(error => { + 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 @@ -95,7 +93,9 @@ export class AuthService { * @param session the current session * @param timestamp timestamp in form of a number */ - private _updateSessionId(credentials: ApiResponseData | ApiResponseError): boolean { + private _updateSessionId( + credentials: ApiResponseData | ApiResponseError + ): boolean { if (credentials instanceof ApiResponseData) { // the dsp api credentials are still valid this.storeToken(credentials.body.message); @@ -108,12 +108,11 @@ export class AuthService { } loadUser(username: string): Observable { - return this.store.dispatch(new LoadUserAction(username)) - .pipe( - tap(() => { - this._isLoggedIn$.next(true); - }) - ); + return this.store.dispatch(new LoadUserAction(username)).pipe( + tap(() => { + this._isLoggedIn$.next(true); + }) + ); } /** @@ -123,42 +122,54 @@ export class AuthService { * @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'; + const identifierType: 'iri' | 'email' | 'username' = + 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); + 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 { - // error handling - if ( - response.status === 401 || - response.status === 403 - ) { - // wrong credentials - return throwError({ - type: 'login', - status: response.status, - msg: 'Wrong credentials', - }); + ), + switchMap( + ( + response: + | ApiResponseData + | ApiResponseError + ) => { + if (response instanceof ApiResponseData) { + return of(true); } else { - // server error - this._errorHandler.showMessage(response); - return throwError({ - type: 'server', - status: response.status, - msg: 'Server error', - }); + // error handling + if ( + response.status === 401 || + response.status === 403 + ) { + // wrong credentials + return throwError({ + type: 'login', + status: response.status, + msg: 'Wrong credentials', + }); + } else { + // server error + return throwError({ + type: 'server', + status: response.status, + msg: 'Server error', + }); + } } } - }) + ) ); } @@ -174,28 +185,28 @@ export class AuthService { logout() { //TODO ? logout by access token missing ? - this._dspApiConnection.v2.auth.logout().pipe( - take(1), - catchError((error: ApiResponseError) => { - this._errorHandler.showMessage(error); - return of(error?.status === 200); - }), - ).subscribe((response: any) => { - if (!(response instanceof ApiResponseData)) { - throwError({ - type: 'server', - status: response.status, - msg: 'Logout was not successful', - }); - return; - } + this._dspApiConnection.v2.auth + .logout() + .pipe( + take(1), + catchError((error: ApiResponseError) => { + return 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(); - } else { - this._errorHandler.showMessage(response.body.message); - } - }); + if (response.body.status === 0) { + this.doLogoutUser(); + } + }); } doLogoutUser() { @@ -214,7 +225,7 @@ export class AuthService { return !!this.getAccessToken(); } - getAccessToken(): string | null { + getAccessToken() { return localStorage.getItem(Auth.AccessToken); } @@ -223,9 +234,9 @@ export class AuthService { } private storeToken(token: string) { - localStorage.setItem(Auth.AccessToken, token); - //localStorage.setItem(this.REFRESH_TOKEN, token); - this.startTokenRefresh(); + localStorage.setItem(Auth.AccessToken, token); + //localStorage.setItem(this.REFRESH_TOKEN, token); + this.startTokenRefresh(); } private refreshAccessToken(access_token: string) { @@ -252,7 +263,10 @@ export class AuthService { return false; } - return date.setSeconds(date.getSeconds() - 30).valueOf() <= new Date().valueOf(); + return ( + date.setSeconds(date.getSeconds() - 30).valueOf() <= + new Date().valueOf() + ); } getTokenExpirationDate(token: string): Date | null { @@ -288,17 +302,10 @@ export class AuthService { const exp = this.getTokenExp(token); const date = new Date(0); date.setUTCSeconds(exp); - const interval = (date as any) - (new Date() as any); if (this.tokenRefreshIntervalId) { clearInterval(this.tokenRefreshIntervalId); } - - //TODO upgrade RxJS to V7 for lastValueFrom support - // this.tokenRefreshIntervalId = - // this.intervalWrapper.setInterval(() => { - // void lastValueFrom(this.refreshToken$()); - // }, interval - 50000); } private getTokenUser(): string { diff --git a/libs/vre/shared/app-state/src/lib/current-project/current-project.selectors.ts b/libs/vre/shared/app-state/src/lib/current-project/current-project.selectors.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/vre/shared/app-state/src/lib/current-project/current-project.state-model.ts b/libs/vre/shared/app-state/src/lib/current-project/current-project.state-model.ts new file mode 100644 index 0000000000..e69de29bb2 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 6082f52deb..2407b33a6d 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,109 +1,81 @@ -import { Inject, Injectable } from '@angular/core'; -import { Action, State, StateContext } from '@ngxs/store'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { ApiResponseData, ApiResponseError, DeleteListNodeResponse, DeleteListResponse, KnoraApiConnection, ListsResponse } from '@dasch-swiss/dsp-js'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; -import { ListsStateModel } from './lists.state-model'; -import { ClearListsAction, DeleteListNodeAction, LoadListsInProjectAction } from './lists.actions'; -import { of } from 'rxjs'; -import { map, take, tap } from 'rxjs/operators'; -import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; - -const defaults: ListsStateModel = { - isLoading: false, - listsInProject: [], -}; - -@State({ - defaults, - name: 'lists', -}) -@Injectable() -export class ListsState { - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _dialog: MatDialog, - ) {} - - @Action(LoadListsInProjectAction) - loadListsInProject( - ctx: StateContext, - { projectIri }: LoadListsInProjectAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.listsEndpoint - .getListsInProject(projectIri) - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - ctx.setState({ ...ctx.getState(), isLoading: false, listsInProject: response.body.lists }); - }, - error: (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - }) - ); - } - - @Action(DeleteListNodeAction) - deleteListNode( - ctx: StateContext, - { listIri }: DeleteListNodeAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.listsEndpoint - .deleteListNode(listIri) - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - ctx.patchState({ isLoading: false }); - }, - error: (error: ApiResponseError) => { - this.handleDeleteError(error); - } - }) - ); - } - - @Action(ClearListsAction) - clearCurrentProject(ctx: StateContext) { - return of(ctx.getState()).pipe( - map(currentState => { - ctx.patchState(defaults); - return currentState; - }) - ); - } - - 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) { - const errorDialogConfig: MatDialogConfig = - { - width: '640px', - position: { - top: '112px', - }, - data: { - mode: 'deleteListNodeError', - }, - }; - - //TODO decouple error handling - //this._dialog.open(DialogComponent, errorDialogConfig); - } else { - // use default error behavior - this._errorHandler.showMessage(error); - } - } -} +import { Injectable } from '@angular/core'; +import { Action, State, StateContext } from '@ngxs/store'; +import { ApiResponseError } from '@dasch-swiss/dsp-js'; +import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; +import { ListsStateModel } from './lists.state-model'; +import { + ClearListsAction, + DeleteListNodeAction, + LoadListsInProjectAction, +} from './lists.actions'; +import { of } from 'rxjs'; +import { finalize, map, take, tap } from 'rxjs/operators'; +import { ListApiService } from '@dasch-swiss/vre/shared/app-api'; + +const defaults: ListsStateModel = { + isLoading: false, + listsInProject: [], +}; + +@State({ + defaults, + name: 'lists', +}) +@Injectable() +export class ListsState { + constructor( + private _listApiService: ListApiService, + private _errorHandler: AppErrorHandler + ) {} + + @Action(LoadListsInProjectAction) + loadListsInProject( + ctx: StateContext, + { projectIri }: LoadListsInProjectAction + ) { + ctx.patchState({ isLoading: true }); + return this._listApiService.listInProject(projectIri).pipe( + take(1), + tap((response) => + ctx.patchState({ listsInProject: response.lists }) + ), + finalize(() => ctx.patchState({ isLoading: false })) + ); + } + + @Action(DeleteListNodeAction) + deleteListNode( + ctx: StateContext, + { listIri }: DeleteListNodeAction + ) { + ctx.patchState({ isLoading: true }); + return this._listApiService.deleteListNode(listIri).pipe( + take(1), + tap({ + next: () => { + ctx.patchState({ isLoading: false }); + }, + error: (error: ApiResponseError) => { + this.handleDeleteError(error); + }, + }) + ); + } + + @Action(ClearListsAction) + clearCurrentProject(ctx: StateContext) { + return of(ctx.getState()).pipe( + map((currentState) => { + ctx.patchState(defaults); + return currentState; + }) + ); + } + + 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 40e347d2fa..60f3988946 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 @@ -1,10 +1,38 @@ import { Inject, Injectable } from '@angular/core'; -import { Action, Actions, State, StateContext, ofActionSuccessful } from '@ngxs/store'; +import { Action, Actions, ofActionSuccessful, State, StateContext } from '@ngxs/store'; import { map, take, tap } from 'rxjs/operators'; import { DspApiConnectionToken, getAllEntityDefinitionsAsArray } from '@dasch-swiss/vre/shared/app-config'; -import { ApiResponseError, Constants, KnoraApiConnection, OntologiesMetadata, ReadOntology, PropertyDefinition, UpdateOntology, UpdateResourceClassCardinality, ResourceClassDefinitionWithAllLanguages, IHasProperty, OntologyMetadata, CanDoResponse } from '@dasch-swiss/dsp-js'; +import { + ApiResponseError, + CanDoResponse, + Constants, + IHasProperty, + KnoraApiConnection, + OntologiesMetadata, + OntologyMetadata, + PropertyDefinition, + ReadOntology, + ResourceClassDefinitionWithAllLanguages, + UpdateOntology, + UpdateResourceClassCardinality +} from '@dasch-swiss/dsp-js'; import { OntologiesStateModel } from './ontologies.state-model'; -import { CurrentOntologyCanBeDeletedAction, ClearCurrentOntologyAction, ClearOntologiesAction, ClearProjectOntologiesAction, LoadOntologyAction, LoadProjectOntologiesAction, RemoveProjectOntologyAction, RemovePropertyAction, ReplacePropertyAction, SetCurrentOntologyAction, SetCurrentProjectOntologyPropertiesAction, SetOntologiesLoadingAction, UpdateOntologyAction, UpdateProjectOntologyAction } from './ontologies.actions'; +import { + ClearCurrentOntologyAction, + ClearOntologiesAction, + ClearProjectOntologiesAction, + CurrentOntologyCanBeDeletedAction, + LoadOntologyAction, + LoadProjectOntologiesAction, + RemoveProjectOntologyAction, + RemovePropertyAction, + ReplacePropertyAction, + SetCurrentOntologyAction, + SetCurrentProjectOntologyPropertiesAction, + SetOntologiesLoadingAction, + UpdateOntologyAction, + UpdateProjectOntologyAction +} from './ontologies.actions'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { IProjectOntologiesKeyValuePairs, OntologyProperties } from '../model-interfaces'; import { of } from 'rxjs'; @@ -13,414 +41,413 @@ import { ProjectService, SortingService } from '@dasch-swiss/vre/shared/app-help import { NotificationService } from '@dasch-swiss/vre/shared/app-notification'; const defaults: OntologiesStateModel = { - isLoading: false, - projectOntologies: {}, - hasLoadingErrors: false, - currentOntology: null, - currentOntologyCanBeDeleted: false, - currentProjectOntologyProperties: [], + isLoading: false, + projectOntologies: {}, + hasLoadingErrors: false, + currentOntology: null, + currentOntologyCanBeDeleted: false, + currentProjectOntologyProperties: [] }; @State({ - defaults, - name: 'ontologies', + defaults, + name: 'ontologies' }) @Injectable() export class OntologiesState { - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler, - private _sortingService: SortingService, - private _projectService: ProjectService, - private _actions$: Actions, - private _notification: NotificationService - ) {} - - //TODO Remove this action when all actions containing this usage is implemented - @Action(SetOntologiesLoadingAction) - setOntologiesLoadingAction( - ctx: StateContext, - { isLoading }: SetOntologiesLoadingAction - ) { - ctx.patchState({ isLoading }); - } + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private _errorHandler: AppErrorHandler, + private _sortingService: SortingService, + private _projectService: ProjectService, + private _actions$: Actions, + private _notification: NotificationService + ) { + } - @Action(SetCurrentOntologyAction) - setCurrentOntologyAction( - ctx: StateContext, - { readOntology }: SetCurrentOntologyAction - ) { - ctx.patchState({ currentOntology: readOntology }); - } + //TODO Remove this action when all actions containing this usage is implemented + @Action(SetOntologiesLoadingAction) + setOntologiesLoadingAction( + ctx: StateContext, + { isLoading }: SetOntologiesLoadingAction + ) { + ctx.patchState({ isLoading }); + } - - @Action(UpdateProjectOntologyAction) - updateProjectOntologyAction( - ctx: StateContext, - { readOntology, projectUuid }: UpdateProjectOntologyAction - ) { - const state = ctx.getState(); - const readOntologies = state.projectOntologies[projectUuid].readOntologies; - readOntologies[readOntologies.findIndex((onto) => onto.id === readOntology.id)] = readOntology; - ctx.patchState(state); - } + @Action(SetCurrentOntologyAction) + setCurrentOntologyAction( + ctx: StateContext, + { readOntology }: SetCurrentOntologyAction + ) { + ctx.patchState({ currentOntology: readOntology }); + } - - @Action(RemoveProjectOntologyAction) - removeProjectOntologyAction( - ctx: StateContext, - { readOntologyId, projectUuid }: RemoveProjectOntologyAction - ) { - const state = ctx.getState(); - const readOntologies = state.projectOntologies[projectUuid].readOntologies; - const index = readOntologies.findIndex((onto) => onto.id === readOntologyId); - if (index > -1) { - readOntologies.splice(index, 1); - } - ctx.patchState(state); - } - @Action(LoadProjectOntologiesAction) - loadProjectOntologiesAction( - ctx: StateContext, - { projectIri }: LoadProjectOntologiesAction - ) { - ctx.patchState({ isLoading: true }); - projectIri = this._projectService.uuidToIri(projectIri); - - // get all project ontologies - return this._dspApiConnection.v2.onto.getOntologiesByProjectIri(projectIri) - .pipe( - take(1), - map((response: OntologiesMetadata | ApiResponseError) => { - return response as OntologiesMetadata; - }), - tap({ - next: (ontoMeta: OntologiesMetadata) => { - if (!ontoMeta.ontologies.length) { - ctx.dispatch(new LoadListsInProjectAction(projectIri)); - ctx.patchState({ isLoading: false }); - return; - } - - const projectOntologies: IProjectOntologiesKeyValuePairs = - { [projectIri] : { ontologiesMetadata: ontoMeta.ontologies, readOntologies: [] } }; - ctx.setState({ ...ctx.getState(), projectOntologies }); - //TODO should load ontologies as a batch with dedicated endpoint, not one by one - ctx.dispatch( - //dispatch all actions except the last one to keep the loading state - ontoMeta.ontologies.slice(0, ontoMeta.ontologies.length - 1) - .map((onto) => new LoadOntologyAction(onto.id, projectIri, false)) - ) - .pipe( - take(1), - tap(() => - ctx.dispatch(new LoadOntologyAction(ontoMeta.ontologies[ontoMeta.ontologies.length - 1].id, projectIri, true)) - ) - ) - .subscribe(() => this._actions$.pipe(ofActionSuccessful(LoadOntologyAction)) - .pipe(take(1)) - .subscribe(() => - //last action dispatched - ctx.dispatch(new LoadListsInProjectAction(projectIri)) - ) - ); - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); - } - }) - ); - } + @Action(UpdateProjectOntologyAction) + updateProjectOntologyAction( + ctx: StateContext, + { readOntology, projectUuid }: UpdateProjectOntologyAction + ) { + const state = ctx.getState(); + const readOntologies = state.projectOntologies[projectUuid].readOntologies; + readOntologies[readOntologies.findIndex((onto) => onto.id === readOntology.id)] = readOntology; + ctx.patchState(state); + } - @Action(LoadOntologyAction) - loadOntologyAction( - ctx: StateContext, - { - ontologyIri: ontologyIri, - projectUuid, - stopLoadingWhenCompleted, - }: LoadOntologyAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.v2.onto.getOntology(ontologyIri, true) - .pipe( - take(1), - map((response: ReadOntology | ApiResponseError) => { - return response as ReadOntology; - }), - tap({ - next: (ontology: ReadOntology) => { - const projectIri = this._projectService.uuidToIri(projectUuid); - let projectOntologiesState = ctx.getState().projectOntologies; - if (!projectOntologiesState[projectIri]) { - projectOntologiesState = { [projectIri]: { ontologiesMetadata: [], readOntologies: [] }} - } - - let projectReadOntologies = projectOntologiesState[projectIri].readOntologies; - projectReadOntologies.push(ontology); - projectReadOntologies = projectReadOntologies.sort((o1, o2) => - this._compareOntologies(o1, o2) - ); - //this._sortingService.keySortByAlphabetical(projectReadOntologies, 'label'); - projectOntologiesState[projectIri].readOntologies = projectReadOntologies; - - ctx.setState({ - ...ctx.getState(), - isLoading: !stopLoadingWhenCompleted, - projectOntologies: projectOntologiesState - }); - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); - } - }) - ); + + @Action(RemoveProjectOntologyAction) + removeProjectOntologyAction( + ctx: StateContext, + { readOntologyId, projectUuid }: RemoveProjectOntologyAction + ) { + const state = ctx.getState(); + const readOntologies = state.projectOntologies[projectUuid].readOntologies; + const index = readOntologies.findIndex((onto) => onto.id === readOntologyId); + if (index > -1) { + readOntologies.splice(index, 1); } + ctx.patchState(state); + } + + @Action(LoadProjectOntologiesAction) + loadProjectOntologiesAction( + ctx: StateContext, + { projectIri }: LoadProjectOntologiesAction + ) { + ctx.patchState({ isLoading: true }); + projectIri = this._projectService.uuidToIri(projectIri); - @Action(UpdateOntologyAction) - updateOntologyAction( - ctx: StateContext, - { ontologyMetadata, projectUuid }: UpdateOntologyAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.v2.onto.updateOntology(ontologyMetadata) - .pipe( + // get all project ontologies + return this._dspApiConnection.v2.onto.getOntologiesByProjectIri(projectIri) + .pipe( + take(1), + map((response: OntologiesMetadata | ApiResponseError) => { + return response as OntologiesMetadata; + }), + tap({ + next: (ontoMeta: OntologiesMetadata) => { + if (!ontoMeta.ontologies.length) { + ctx.dispatch(new LoadListsInProjectAction(projectIri)); + ctx.patchState({ isLoading: false }); + return; + } + + const projectOntologies: IProjectOntologiesKeyValuePairs = + { [projectIri]: { ontologiesMetadata: ontoMeta.ontologies, readOntologies: [] } }; + ctx.setState({ ...ctx.getState(), projectOntologies }); + //TODO should load ontologies as a batch with dedicated endpoint, not one by one + ctx.dispatch( + //dispatch all actions except the last one to keep the loading state + ontoMeta.ontologies.slice(0, ontoMeta.ontologies.length - 1) + .map((onto) => new LoadOntologyAction(onto.id, projectIri, false)) + ) + .pipe( take(1), - map((response: OntologyMetadata | ApiResponseError) => { - return response as OntologyMetadata; - }), - tap({ - next: (ontology: OntologyMetadata) => { - const projectIri = this._projectService.uuidToIri(projectUuid); - ctx.dispatch(new LoadProjectOntologiesAction(projectIri)); - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true }); - this._errorHandler.showMessage(error); - } - }) + tap(() => + ctx.dispatch(new LoadOntologyAction(ontoMeta.ontologies[ontoMeta.ontologies.length - 1].id, projectIri, true)) + ) + ) + .subscribe(() => this._actions$.pipe(ofActionSuccessful(LoadOntologyAction)) + .pipe(take(1)) + .subscribe(() => + //last action dispatched + ctx.dispatch(new LoadListsInProjectAction(projectIri)) + ) + ); + }, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true, isLoading: false }); + this._errorHandler.showMessage(error); + } + }) + ); + } + + @Action(LoadOntologyAction) + loadOntologyAction( + ctx: StateContext, + { + ontologyIri: ontologyIri, + projectUuid, + stopLoadingWhenCompleted + }: LoadOntologyAction + ) { + ctx.patchState({ isLoading: true }); + return this._dspApiConnection.v2.onto.getOntology(ontologyIri, true) + .pipe( + take(1), + map((response: ReadOntology | ApiResponseError) => { + return response as ReadOntology; + }), + tap({ + next: (ontology: ReadOntology) => { + const projectIri = this._projectService.uuidToIri(projectUuid); + let projectOntologiesState = ctx.getState().projectOntologies; + if (!projectOntologiesState[projectIri]) { + projectOntologiesState = { [projectIri]: { ontologiesMetadata: [], readOntologies: [] } }; + } + + let projectReadOntologies = projectOntologiesState[projectIri].readOntologies; + projectReadOntologies.push(ontology); + projectReadOntologies = projectReadOntologies.sort((o1, o2) => + this._compareOntologies(o1, o2) ); - } - - @Action(ClearProjectOntologiesAction) - clearProjectOntologies( - ctx: StateContext, - { projectUuid }: ClearProjectOntologiesAction - ) { - const projectIri = this._projectService.uuidToIri(projectUuid); - return of(ctx.getState()).pipe( - map(currentState => { - if (currentState.projectOntologies[projectIri]) { - currentState.projectOntologies[projectIri].ontologiesMetadata = []; - currentState.projectOntologies[projectIri].readOntologies = []; - ctx.patchState(currentState); - } - - return currentState; - }) - ); - } - - @Action(ClearOntologiesAction) - clearOntologies(ctx: StateContext) { - return of(ctx.getState()).pipe( - map(currentState => { - ctx.patchState(defaults); - return currentState; - }) - ); - } + //this._sortingService.keySortByAlphabetical(projectReadOntologies, 'label'); + projectOntologiesState[projectIri].readOntologies = projectReadOntologies; - @Action(ClearCurrentOntologyAction) - clearCurrentOntology(ctx: StateContext) { - const state = ctx.getState(); - state.currentOntology = defaults.currentOntology; - state.currentProjectOntologyProperties = defaults.currentProjectOntologyProperties; - ctx.patchState(state); - } - - // don't log error to rollbar if 'currentProjectOntologies' does not exist in the application state - @Action(SetCurrentProjectOntologyPropertiesAction) - setCurrentProjectOntologyPropertiesAction( - ctx: StateContext, - { projectUuid }: SetCurrentProjectOntologyPropertiesAction - ) { - const state = ctx.getState(); - if (!state.projectOntologies[projectUuid]) { - return; + ctx.setState({ + ...ctx.getState(), + isLoading: !stopLoadingWhenCompleted, + projectOntologies: projectOntologiesState + }); + }, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true, isLoading: false }); + this._errorHandler.showMessage(error); + } + }) + ); + } + + @Action(UpdateOntologyAction) + updateOntologyAction( + ctx: StateContext, + { ontologyMetadata, projectUuid }: UpdateOntologyAction + ) { + ctx.patchState({ isLoading: true }); + return this._dspApiConnection.v2.onto.updateOntology(ontologyMetadata) + .pipe( + take(1), + map((response: OntologyMetadata | ApiResponseError) => { + return response as OntologyMetadata; + }), + tap({ + next: () => { + const projectIri = this._projectService.uuidToIri(projectUuid); + ctx.dispatch(new LoadProjectOntologiesAction(projectIri)); + }, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true }); + this._errorHandler.showMessage(error); + } + }) + ); + } + + @Action(ClearProjectOntologiesAction) + clearProjectOntologies( + ctx: StateContext, + { projectUuid }: ClearProjectOntologiesAction + ) { + const projectIri = this._projectService.uuidToIri(projectUuid); + return of(ctx.getState()).pipe( + map(currentState => { + if (currentState.projectOntologies[projectIri]) { + currentState.projectOntologies[projectIri].ontologiesMetadata = []; + currentState.projectOntologies[projectIri].readOntologies = []; + ctx.patchState(currentState); } - // get all project ontologies - const projectOntologies = state.projectOntologies[projectUuid].readOntologies; - const ontoProperties = projectOntologies.map((onto) => { - ontology: onto.id, - properties: this.initOntoProperties(getAllEntityDefinitionsAsArray(onto.properties)), - }); - - ctx.setState({ ...state, currentProjectOntologyProperties: ontoProperties }); - } - /** - * removes property from resource class - * @param property - */ - @Action(RemovePropertyAction) - removePropertyAction( - ctx: StateContext, - { property, resourceClass, currentOntologyPropertiesToDisplay }: RemovePropertyAction - ) { - ctx.patchState({ isLoading: true }); - const state = ctx.getState(); - - const onto = new UpdateOntology(); - onto.lastModificationDate = state.currentOntology?.lastModificationDate; - onto.id = state.currentOntology?.id; - const delCard = new UpdateResourceClassCardinality(); - delCard.id = resourceClass.id; - delCard.cardinalities = []; - delCard.cardinalities = currentOntologyPropertiesToDisplay.filter( - (prop) => prop.propertyIndex === property.iri - ); - onto.entity = delCard; - - return this._dspApiConnection.v2.onto.deleteCardinalityFromResourceClass(onto) - .pipe( - take(1), - map((response: ResourceClassDefinitionWithAllLanguages | ApiResponseError) => - response as ResourceClassDefinitionWithAllLanguages - ), - tap({ - next: (res: ResourceClassDefinitionWithAllLanguages) => { - //ctx.dispatch(new SetCurrentOntologyPropertiesToDisplayAction(currentOntologyPropertiesToDisplay)); - ctx.setState({ ...state, isLoading: false }); - this._notification.openSnackBar( - `You have successfully removed "${property.label}" from "${resourceClass.label}".` - ); - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); - } - }) - ); - } + return currentState; + }) + ); + } - @Action(ReplacePropertyAction) - replacePropertyAction( - ctx: StateContext, - { resourceClass, currentOntologyPropertiesToDisplay }: ReplacePropertyAction - ) { - ctx.patchState({ isLoading: true }); - const state = ctx.getState(); - - const onto = new UpdateOntology(); - onto.lastModificationDate = state.currentOntology?.lastModificationDate; - onto.id = state.currentOntology?.id; - const addCard = new UpdateResourceClassCardinality(); - addCard.id = resourceClass.id; - addCard.cardinalities = []; - currentOntologyPropertiesToDisplay.forEach((prop, index) => { - const propCard: IHasProperty = { - propertyIndex: prop.propertyIndex, - cardinality: prop.cardinality, - guiOrder: index + 1, - }; - - addCard.cardinalities.push(propCard); - }); - - onto.entity = addCard; - - - return this._dspApiConnection.v2.onto.replaceGuiOrderOfCardinalities(onto) - .pipe( - take(1), - map((response: ResourceClassDefinitionWithAllLanguages | ApiResponseError) => - response as ResourceClassDefinitionWithAllLanguages - ), - tap({ - next: (res: ResourceClassDefinitionWithAllLanguages) => { - //TODO lastModificationDate should be updated in state if reload action is not executed after this - //this.lastModificationDate = responseGuiOrder.lastModificationDate; - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true, isLoading: false }); - this._errorHandler.showMessage(error); - } - }) - ); + @Action(ClearOntologiesAction) + clearOntologies(ctx: StateContext) { + return of(ctx.getState()).pipe( + map(currentState => { + ctx.patchState(defaults); + return currentState; + }) + ); + } + + @Action(ClearCurrentOntologyAction) + clearCurrentOntology(ctx: StateContext) { + const state = ctx.getState(); + state.currentOntology = defaults.currentOntology; + state.currentProjectOntologyProperties = defaults.currentProjectOntologyProperties; + ctx.patchState(state); + } + + // don't log error to rollbar if 'currentProjectOntologies' does not exist in the application state + @Action(SetCurrentProjectOntologyPropertiesAction) + setCurrentProjectOntologyPropertiesAction( + ctx: StateContext, + { projectUuid }: SetCurrentProjectOntologyPropertiesAction + ) { + const state = ctx.getState(); + if (!state.projectOntologies[projectUuid]) { + return; } + // get all project ontologies + const projectOntologies = state.projectOntologies[projectUuid].readOntologies; + const ontoProperties = projectOntologies.map((onto) => { + ontology: onto.id, + properties: this.initOntoProperties(getAllEntityDefinitionsAsArray(onto.properties)) + }); - - @Action(CurrentOntologyCanBeDeletedAction) - currentOntologyCanBeDeletedAction( - ctx: StateContext) { - ctx.patchState({ isLoading: true }); - const state = ctx.getState(); - if (!state.currentOntology) { - return; - } + ctx.setState({ ...state, currentProjectOntologyProperties: ontoProperties }); + } - return this._dspApiConnection.v2.onto.canDeleteOntology(state.currentOntology.id) - .pipe( - take(1), - map((response: CanDoResponse | ApiResponseError) => { - return response as CanDoResponse; - }), - tap({ - next: (response: CanDoResponse) => { - ctx.setState({ - ...ctx.getState(), - isLoading: false, - currentOntologyCanBeDeleted: response.canDo - }); - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true }); - this._errorHandler.showMessage(error); - } - }) + /** + * removes property from resource class + * @param property + */ + @Action(RemovePropertyAction) + removePropertyAction( + ctx: StateContext, + { property, resourceClass, currentOntologyPropertiesToDisplay }: RemovePropertyAction + ) { + ctx.patchState({ isLoading: true }); + const state = ctx.getState(); + + const onto = new UpdateOntology(); + onto.lastModificationDate = state.currentOntology?.lastModificationDate; + onto.id = state.currentOntology?.id; + const delCard = new UpdateResourceClassCardinality(); + delCard.id = resourceClass.id; + delCard.cardinalities = []; + delCard.cardinalities = currentOntologyPropertiesToDisplay.filter( + (prop) => prop.propertyIndex === property.iri + ); + onto.entity = delCard; + + return this._dspApiConnection.v2.onto.deleteCardinalityFromResourceClass(onto) + .pipe( + take(1), + map((response: ResourceClassDefinitionWithAllLanguages | ApiResponseError) => + response as ResourceClassDefinitionWithAllLanguages + ), + tap({ + next: () => { + //ctx.dispatch(new SetCurrentOntologyPropertiesToDisplayAction(currentOntologyPropertiesToDisplay)); + ctx.setState({ ...state, isLoading: false }); + this._notification.openSnackBar( + `You have successfully removed "${property.label}" from "${resourceClass.label}".` ); - } + }, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true, isLoading: false }); + this._errorHandler.showMessage(error); + } + }) + ); + } - /** - * compare function which sorts the ontologies in the ascending order. - * - * @param o1 ontology 1 - * @param o2 ontology 2 - * @private - */ - private _compareOntologies(o1: ReadOntology, o2: ReadOntology) { - if (o1.label > o2.label) { - return 1; - } + @Action(ReplacePropertyAction) + replacePropertyAction( + ctx: StateContext, + { resourceClass, currentOntologyPropertiesToDisplay }: ReplacePropertyAction + ) { + ctx.patchState({ isLoading: true }); + const state = ctx.getState(); - if (o1.label < o2.label) { - return -1; - } + const onto = new UpdateOntology(); + onto.lastModificationDate = state.currentOntology?.lastModificationDate; + onto.id = state.currentOntology?.id; + const addCard = new UpdateResourceClassCardinality(); + addCard.id = resourceClass.id; + addCard.cardinalities = []; + currentOntologyPropertiesToDisplay.forEach((prop, index) => { + const propCard: IHasProperty = { + propertyIndex: prop.propertyIndex, + cardinality: prop.cardinality, + guiOrder: index + 1 + }; - return 0; + addCard.cardinalities.push(propCard); + }); + + onto.entity = addCard; + + + return this._dspApiConnection.v2.onto.replaceGuiOrderOfCardinalities(onto) + .pipe( + take(1), + map((response: ResourceClassDefinitionWithAllLanguages | ApiResponseError) => + response as ResourceClassDefinitionWithAllLanguages + ), + tap({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + next: () => {}, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true, isLoading: false }); + this._errorHandler.showMessage(error); + } + }) + ); + } + + + @Action(CurrentOntologyCanBeDeletedAction) + currentOntologyCanBeDeletedAction( + ctx: StateContext) { + ctx.patchState({ isLoading: true }); + const state = ctx.getState(); + if (!state.currentOntology) { + return; } - private initOntoProperties(allOntoProperties: PropertyDefinition[]): PropertyDefinition[] { - // reset the ontology properties - const listOfProperties: PropertyDefinition[] = []; - - // display only the properties which are not a subjectType of Standoff - allOntoProperties.forEach((resProp) => { - const standoff = resProp.subjectType - ? resProp.subjectType.includes('Standoff') - : false; - if (resProp.objectType !== Constants.LinkValue && !standoff) { - listOfProperties.push(resProp); - } - }); + return this._dspApiConnection.v2.onto.canDeleteOntology(state.currentOntology.id) + .pipe( + take(1), + map((response: CanDoResponse | ApiResponseError) => { + return response as CanDoResponse; + }), + tap({ + next: (response: CanDoResponse) => { + ctx.setState({ + ...ctx.getState(), + isLoading: false, + currentOntologyCanBeDeleted: response.canDo + }); + }, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true }); + this._errorHandler.showMessage(error); + } + }) + ); + } - // sort properties by label - // --> TODO: add sort functionallity to the gui - return this._sortingService.keySortByAlphabetical(listOfProperties, 'label'); + /** + * compare function which sorts the ontologies in the ascending order. + * + * @param o1 ontology 1 + * @param o2 ontology 2 + * @private + */ + private _compareOntologies(o1: ReadOntology, o2: ReadOntology) { + if (o1.label > o2.label) { + return 1; } + + if (o1.label < o2.label) { + return -1; + } + + return 0; + } + + private initOntoProperties(allOntoProperties: PropertyDefinition[]): PropertyDefinition[] { + // reset the ontology properties + const listOfProperties: PropertyDefinition[] = []; + + // display only the properties which are not a subjectType of Standoff + allOntoProperties.forEach((resProp) => { + const standoff = resProp.subjectType + ? resProp.subjectType.includes('Standoff') + : false; + if (resProp.objectType !== Constants.LinkValue && !standoff) { + listOfProperties.push(resProp); + } + }); + + // sort properties by label + // --> TODO: add sort functionallity to the gui + return this._sortingService.keySortByAlphabetical(listOfProperties, 'label'); + } } diff --git a/libs/vre/shared/app-state/src/lib/projects/projects.state-model.ts b/libs/vre/shared/app-state/src/lib/projects/projects.state-model.ts index b4d5b5ad60..5b61605e75 100644 --- a/libs/vre/shared/app-state/src/lib/projects/projects.state-model.ts +++ b/libs/vre/shared/app-state/src/lib/projects/projects.state-model.ts @@ -1,11 +1,11 @@ -import { ReadGroup, ReadProject, ReadUser, StoredProject } from "@dasch-swiss/dsp-js"; -import { IKeyValuePairs } from "../model-interfaces"; +import { ReadGroup, ReadProject, ReadUser } from '@dasch-swiss/dsp-js'; +import { IKeyValuePairs } from '../model-interfaces'; export class ProjectsStateModel { - isLoading = false; - hasLoadingErrors = false; - allProjects: ReadProject[] = []; - readProjects: ReadProject[] = []; - projectMembers: IKeyValuePairs = {}; - projectGroups: IKeyValuePairs = {}; + isLoading = false; + hasLoadingErrors = false; + allProjects: ReadProject[] = []; + readProjects: ReadProject[] = []; + projectMembers: IKeyValuePairs = {}; + projectGroups: IKeyValuePairs = {}; } 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 6b0a30d01e..ac4d0ae5ba 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 @@ -1,294 +1,294 @@ -import { Inject, Injectable } from '@angular/core'; -import { Action, State, StateContext, Store } from '@ngxs/store'; -import { ProjectsStateModel } from './projects.state-model'; -import { LoadProjectsAction, LoadProjectAction, ClearProjectsAction, RemoveUserFromProjectAction, AddUserToProjectMembershipAction, LoadProjectMembersAction, LoadProjectGroupsAction, UpdateProjectAction, SetProjectMemberAction } from './projects.actions'; -import { UserSelectors } from '../user/user.selectors'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { ApiResponseData, ApiResponseError, GroupsResponse, KnoraApiConnection, MembersResponse, ProjectResponse, ProjectsResponse, ReadGroup, UserResponse } from '@dasch-swiss/dsp-js'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; -import { concatMap, finalize, map, take, tap } from 'rxjs/operators'; -import { produce } from 'immer'; -import { EMPTY, of } from 'rxjs'; -import { IKeyValuePairs } from '../model-interfaces'; -import { SetUserAction } from '../user/user.actions'; -import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; - -const defaults: ProjectsStateModel = { - isLoading: false, - hasLoadingErrors: false, - allProjects: [], - readProjects: [], - projectMembers: {}, - projectGroups: {}, -}; - -@State({ - defaults, - name: 'projects', -}) -@Injectable() -export class ProjectsState { - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private store: Store, - private errorHandler: AppErrorHandler, - private projectService: ProjectService, - ) {} - - @Action(LoadProjectsAction, { cancelUncompleted: true }) - loadProjects( - ctx: StateContext) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.projectsEndpoint - .getProjects() - .pipe( - take(1), - map((projectsResponse: ApiResponseData | ApiResponseError) => { - return projectsResponse as ApiResponseData; - }), - tap({ - next:(projectsResponse: ApiResponseData) => { - ctx.setState({ - ...ctx.getState(), - isLoading: false, - allProjects: projectsResponse.body.projects, - }); - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); - } - }), - finalize(() => { - ctx.patchState({ isLoading: false }); - }) - ); - } - - @Action(LoadProjectAction, { cancelUncompleted: true }) - loadProjectAction( - ctx: StateContext, - { projectUuid, isCurrentProject }: LoadProjectAction - ) { - ctx.patchState({ isLoading: true }); - - const projectIri = this.projectService.uuidToIri(projectUuid); - // get current project data, project members and project groups - // and set the project state here - return this._dspApiConnection.admin.projectsEndpoint - .getProjectByIri(projectIri) - .pipe( - take(1), - map((projectsResponse: ApiResponseData | ApiResponseError) => { - return projectsResponse as ApiResponseData; - }), - tap({ - next:(response: ApiResponseData) => { - const project = response.body.project; - - let state = ctx.getState(); - if (!state.readProjects) { - state.readProjects = []; - } - - state = produce(state, draft => { - const index = draft.readProjects.findIndex(p => p.id === project.id); - index > -1 - ? draft.readProjects[index] = project - : draft.readProjects.push(project); - draft.isLoading = false; - }); - - ctx.patchState(state); - return project; - }, - error: (error: ApiResponseError) => { - ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); - } - }), - concatMap(() => ctx.dispatch([ - new LoadProjectMembersAction(projectUuid), - new LoadProjectGroupsAction(projectUuid) - ])), - finalize(() => { - ctx.patchState({ isLoading: false }); - }) - ); - } - - @Action(ClearProjectsAction) - clearProjects(ctx: StateContext) { - return of(ctx.getState()).pipe( - map(currentState => { - defaults.allProjects = currentState.allProjects as any; - ctx.patchState(defaults); - return currentState; - }) - ); - } - - @Action(RemoveUserFromProjectAction) - removeUserFromProject( - ctx: StateContext, - { userId, projectIri }: RemoveUserFromProjectAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.usersEndpoint - .removeUserFromProjectMembership(userId, projectIri) - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - ctx.dispatch([ - new SetUserAction(response.body.user), - new LoadProjectMembersAction(projectIri) - ]); - ctx.patchState({ isLoading: false }); - }, - error: (error) => { - this.errorHandler.showMessage(error); - } - }) - ) - } - - @Action(AddUserToProjectMembershipAction) - addUserToProjectMembership( - ctx: StateContext, - { userId, projectIri }: AddUserToProjectMembershipAction - ) { - ctx.patchState({ isLoading: true, hasLoadingErrors: false }); - return this._dspApiConnection.admin.usersEndpoint - .addUserToProjectMembership(userId, projectIri) - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - ctx.dispatch([ - new SetUserAction(response.body.user), - new LoadProjectMembersAction(projectIri) - ]); - ctx.patchState({ isLoading: false }); - }, - error: (error) => { - ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); - } - }) - ) - } - - @Action(LoadProjectMembersAction) - loadProjectMembersAction( - ctx: StateContext, - { projectUuid }: LoadProjectMembersAction - ) { - if (!this.store.selectSnapshot(UserSelectors.isLoggedIn)) { - return; - } - - ctx.patchState({ isLoading: true }); - const projectIri = this.projectService.uuidToIri(projectUuid); - return this._dspApiConnection.admin.projectsEndpoint.getProjectMembersByIri(projectIri) - .pipe( - take(1), - map((membersResponse: ApiResponseData | ApiResponseError) => { - return membersResponse as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - ctx.setState({ - ...ctx.getState(), - isLoading: false, - projectMembers: { [projectIri] : { value: response.body.members } } - }); - }, - error: (error) => { - ctx.patchState({ hasLoadingErrors: true }); - this.errorHandler.showMessage(error); - } - }) - ); - } - - @Action(LoadProjectGroupsAction) - loadProjectGroupsAction(ctx: StateContext) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.groupsEndpoint.getGroups() - .pipe( - take(1), - map((groupsResponse: ApiResponseData | ApiResponseError) => { - return groupsResponse as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - const groups: IKeyValuePairs = {}; - response.body.groups.forEach(group => { - const projectId = group.project?.id as string; - if (!groups[projectId]) { - groups[projectId] = { value: [] }; - } - - groups[projectId].value = [...groups[projectId].value, group]; - }); - - ctx.setState({ - ...ctx.getState(), - isLoading: false, - projectGroups: groups - }); - }, - error: (error) => { - this.errorHandler.showMessage(error); - } - }) - ); - } - - @Action(UpdateProjectAction) - updateProjectAction( - ctx: StateContext, - { projectUuid, projectData }: UpdateProjectAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.projectsEndpoint.updateProject(projectUuid, projectData) - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - ctx.dispatch(new LoadProjectsAction()); - return response.body.project; - }, - error: (error) => { - this.errorHandler.showMessage(error); - } - }) - ); - } - - @Action(SetProjectMemberAction) - setProjectMember(ctx: StateContext, - { member }: SetProjectMemberAction - ) { - const state = ctx.getState(); - Object.keys(state.projectMembers).forEach((projectId) => { - const index = state.projectMembers[projectId].value.findIndex(u => u.id === member.id); - if (index > -1) { - state.projectMembers[projectId].value[index] = member; - } - }); - - ctx.setState({ ...state }); - } -} +import { Inject, Injectable } from '@angular/core'; +import { Action, State, StateContext, Store } from '@ngxs/store'; +import { ProjectsStateModel } from './projects.state-model'; +import { LoadProjectsAction, LoadProjectAction, ClearProjectsAction, RemoveUserFromProjectAction, AddUserToProjectMembershipAction, LoadProjectMembersAction, LoadProjectGroupsAction, UpdateProjectAction, SetProjectMemberAction } from './projects.actions'; +import { UserSelectors } from '../user/user.selectors'; +import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; +import { ApiResponseData, ApiResponseError, GroupsResponse, KnoraApiConnection, MembersResponse, ProjectResponse, ProjectsResponse, ReadGroup, UserResponse } from '@dasch-swiss/dsp-js'; +import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; +import { concatMap, finalize, map, take, tap } from 'rxjs/operators'; +import { produce } from 'immer'; +import { EMPTY, of } from 'rxjs'; +import { IKeyValuePairs } from '../model-interfaces'; +import { SetUserAction } from '../user/user.actions'; +import { ProjectService } from '@dasch-swiss/vre/shared/app-helper-services'; + +const defaults: ProjectsStateModel = { + isLoading: false, + hasLoadingErrors: false, + allProjects: [], + readProjects: [], + projectMembers: {}, + projectGroups: {}, +}; + +@State({ + defaults, + name: 'projects', +}) +@Injectable() +export class ProjectsState { + constructor( + @Inject(DspApiConnectionToken) + private _dspApiConnection: KnoraApiConnection, + private store: Store, + private errorHandler: AppErrorHandler, + private projectService: ProjectService, + ) {} + + @Action(LoadProjectsAction, { cancelUncompleted: true }) + loadProjects( + ctx: StateContext) { + ctx.patchState({ isLoading: true }); + return this._dspApiConnection.admin.projectsEndpoint + .getProjects() + .pipe( + take(1), + map((projectsResponse: ApiResponseData | ApiResponseError) => { + return projectsResponse as ApiResponseData; + }), + tap({ + next:(projectsResponse: ApiResponseData) => { + ctx.setState({ + ...ctx.getState(), + isLoading: false, + allProjects: projectsResponse.body.projects, + }); + }, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true }); + this.errorHandler.showMessage(error); + } + }), + finalize(() => { + ctx.patchState({ isLoading: false }); + }) + ); + } + + @Action(LoadProjectAction, { cancelUncompleted: true }) + loadProjectAction( + ctx: StateContext, + { projectUuid, isCurrentProject }: LoadProjectAction + ) { + ctx.patchState({ isLoading: true }); + + const projectIri = this.projectService.uuidToIri(projectUuid); + // get current project data, project members and project groups + // and set the project state here + return this._dspApiConnection.admin.projectsEndpoint + .getProjectByIri(projectIri) + .pipe( + take(1), + map((projectsResponse: ApiResponseData | ApiResponseError) => { + return projectsResponse as ApiResponseData; + }), + tap({ + next:(response: ApiResponseData) => { + const project = response.body.project; + + let state = ctx.getState(); + if (!state.readProjects) { + state.readProjects = []; + } + + state = produce(state, draft => { + const index = draft.readProjects.findIndex(p => p.id === project.id); + index > -1 + ? draft.readProjects[index] = project + : draft.readProjects.push(project); + draft.isLoading = false; + }); + + ctx.patchState(state); + return project; + }, + error: (error: ApiResponseError) => { + ctx.patchState({ hasLoadingErrors: true }); + this.errorHandler.showMessage(error); + } + }), + concatMap(() => ctx.dispatch([ + new LoadProjectMembersAction(projectUuid), + new LoadProjectGroupsAction(projectUuid) + ])), + finalize(() => { + ctx.patchState({ isLoading: false }); + }) + ); + } + + @Action(ClearProjectsAction) + clearProjects(ctx: StateContext) { + return of(ctx.getState()).pipe( + map(currentState => { + defaults.allProjects = currentState.allProjects as any; + ctx.patchState(defaults); + return currentState; + }) + ); + } + + @Action(RemoveUserFromProjectAction) + removeUserFromProject( + ctx: StateContext, + { userId, projectIri }: RemoveUserFromProjectAction + ) { + ctx.patchState({ isLoading: true }); + return this._dspApiConnection.admin.usersEndpoint + .removeUserFromProjectMembership(userId, projectIri) + .pipe( + take(1), + map((response: ApiResponseData | ApiResponseError) => { + return response as ApiResponseData; + }), + tap({ + next: (response: ApiResponseData) => { + ctx.dispatch([ + new SetUserAction(response.body.user), + new LoadProjectMembersAction(projectIri) + ]); + ctx.patchState({ isLoading: false }); + }, + error: (error) => { + this.errorHandler.showMessage(error); + } + }) + ) + } + + @Action(AddUserToProjectMembershipAction) + addUserToProjectMembership( + ctx: StateContext, + { userId, projectIri }: AddUserToProjectMembershipAction + ) { + ctx.patchState({ isLoading: true, hasLoadingErrors: false }); + return this._dspApiConnection.admin.usersEndpoint + .addUserToProjectMembership(userId, projectIri) + .pipe( + take(1), + map((response: ApiResponseData | ApiResponseError) => { + return response as ApiResponseData; + }), + tap({ + next: (response: ApiResponseData) => { + ctx.dispatch([ + new SetUserAction(response.body.user), + new LoadProjectMembersAction(projectIri) + ]); + ctx.patchState({ isLoading: false }); + }, + error: (error) => { + ctx.patchState({ hasLoadingErrors: true }); + this.errorHandler.showMessage(error); + } + }) + ) + } + + @Action(LoadProjectMembersAction) + loadProjectMembersAction( + ctx: StateContext, + { projectUuid }: LoadProjectMembersAction + ) { + if (!this.store.selectSnapshot(UserSelectors.isLoggedIn)) { + return; + } + + ctx.patchState({ isLoading: true }); + const projectIri = this.projectService.uuidToIri(projectUuid); + return this._dspApiConnection.admin.projectsEndpoint.getProjectMembersByIri(projectIri) + .pipe( + take(1), + map((membersResponse: ApiResponseData | ApiResponseError) => { + return membersResponse as ApiResponseData; + }), + tap({ + next: (response: ApiResponseData) => { + ctx.setState({ + ...ctx.getState(), + isLoading: false, + projectMembers: { [projectIri] : { value: response.body.members } } + }); + }, + error: (error) => { + ctx.patchState({ hasLoadingErrors: true }); + this.errorHandler.showMessage(error); + } + }) + ); + } + + @Action(LoadProjectGroupsAction) + loadProjectGroupsAction(ctx: StateContext) { + ctx.patchState({ isLoading: true }); + return this._dspApiConnection.admin.groupsEndpoint.getGroups() + .pipe( + take(1), + map((groupsResponse: ApiResponseData | ApiResponseError) => { + return groupsResponse as ApiResponseData; + }), + tap({ + next: (response: ApiResponseData) => { + const groups: IKeyValuePairs = {}; + response.body.groups.forEach(group => { + const projectId = group.project?.id as string; + if (!groups[projectId]) { + groups[projectId] = { value: [] }; + } + + groups[projectId].value = [...groups[projectId].value, group]; + }); + + ctx.setState({ + ...ctx.getState(), + isLoading: false, + projectGroups: groups + }); + }, + error: (error) => { + this.errorHandler.showMessage(error); + } + }) + ); + } + + @Action(UpdateProjectAction) + updateProjectAction( + ctx: StateContext, + { projectUuid, projectData }: UpdateProjectAction + ) { + ctx.patchState({ isLoading: true }); + return this._dspApiConnection.admin.projectsEndpoint.updateProject(projectUuid, projectData) + .pipe( + take(1), + map((response: ApiResponseData | ApiResponseError) => { + return response as ApiResponseData; + }), + tap({ + next: (response: ApiResponseData) => { + ctx.dispatch(new LoadProjectsAction()); + return response.body.project; + }, + error: (error) => { + this.errorHandler.showMessage(error); + } + }) + ); + } + + @Action(SetProjectMemberAction) + setProjectMember(ctx: StateContext, + { member }: SetProjectMemberAction + ) { + const state = ctx.getState(); + Object.keys(state.projectMembers).forEach((projectId) => { + const index = state.projectMembers[projectId].value.findIndex(u => u.id === member.id); + if (index > -1) { + state.projectMembers[projectId].value[index] = member; + } + }); + + ctx.setState({ ...state }); + } +} 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 b2603dce29..7ceb5605d4 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,9 +1,9 @@ -import { ReadUser, User } from "@dasch-swiss/dsp-js"; - -export class UserStateModel { - isLoading: boolean | undefined; - user: User | ReadUser | null | undefined; - userProjectAdminGroups: string[] = []; //before was projectAdmin - isMemberOfSystemAdminGroup = false; //before was sysAdmin - allUsers: ReadUser[] = []; -} +import { ReadUser, User } from '@dasch-swiss/dsp-js'; + +export class UserStateModel { + isLoading: boolean | undefined; + user: User | ReadUser | null | undefined; + 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 e5baef344e..59eb728691 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 @@ -3,234 +3,217 @@ import { Action, State, StateContext } from '@ngxs/store'; import { of } from 'rxjs'; import { map, take, tap } from 'rxjs/operators'; import { - LogUserOutAction, - LoadUserAction, - LoadUsersAction, - ResetUsersAction as ResetUsersAction, - CreateUserAction, - SetUserAction, - RemoveUserAction, - LoadUserContentByIriAction, - SetUserProjectGroupsAction, + CreateUserAction, + LoadUserAction, + LoadUserContentByIriAction, + LoadUsersAction, + LogUserOutAction, + RemoveUserAction, + ResetUsersAction as ResetUsersAction, + SetUserAction, + SetUserProjectGroupsAction } from './user.actions'; import { UserStateModel } from './user.state-model'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { ApiResponseData, ApiResponseError, Constants, KnoraApiConnection, ReadUser, User, UserResponse, UsersResponse } from '@dasch-swiss/dsp-js'; +import { ApiResponseError, Constants, ReadUser } from '@dasch-swiss/dsp-js'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { SetProjectMemberAction } from '../projects/projects.actions'; +import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; const defaults = { - isLoading: false, - user: null, - userProjectAdminGroups: [], - isMemberOfSystemAdminGroup: false, - allUsers: [], + isLoading: false, + user: null, + userProjectAdminGroups: [], + isMemberOfSystemAdminGroup: false, + allUsers: [] }; @State({ - defaults, - name: 'user', + defaults, + name: 'user' }) @Injectable() export class UserState { - constructor( - @Inject(DspApiConnectionToken) - private _dspApiConnection: KnoraApiConnection, - private _errorHandler: AppErrorHandler - ) {} - - @Action(LoadUserAction) - loadUser( - ctx: StateContext, - { username }: LoadUserAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.usersEndpoint - .getUserByUsername(username) - .pipe( - take(1), - map((response: - | ApiResponseData - | ApiResponseError - ) => { - if (response instanceof ApiResponseData) { - ctx.setState({ ...ctx.getState(), isLoading: false, user: response.body.user }); - ctx.dispatch(new SetUserProjectGroupsAction(response.body.user)); - return response.body.user; - } else { - console.error(response); - return new User; - } - }) - ) - } + constructor( + private _userApiService: UserApiService, + private _errorHandler: AppErrorHandler + ) { + } + + @Action(LoadUserAction) + loadUser( + ctx: StateContext, + { username }: LoadUserAction + ) { + ctx.patchState({ isLoading: true }); + return this._userApiService.get(username, 'username') + .pipe( + take(1), + map(response => { + ctx.setState({ ...ctx.getState(), isLoading: false, user: response.user }); + ctx.dispatch(new SetUserProjectGroupsAction(response.user)); + return response.user; + }) + ); + } + + @Action(LoadUserContentByIriAction) + loadUserContentByIriAction( + ctx: StateContext, + { iri }: LoadUserContentByIriAction + ) { + ctx.patchState({ isLoading: true }); + return this._userApiService.get(iri) + .pipe( + take(1), + tap({ + next: response => { + const state = ctx.getState(); + const userIndex = state.allUsers.findIndex(u => u.id === response.user.id); + if (userIndex > -1) { + state.allUsers[userIndex] = response.user; + } - @Action(LoadUserContentByIriAction) - loadUserContentByIriAction( - ctx: StateContext, - { iri }: LoadUserContentByIriAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.usersEndpoint.getUserByIri(iri) - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (responseUser: ApiResponseData) => { - const state = ctx.getState(); - const userIndex = state.allUsers.findIndex(u => u.id === responseUser.body.user.id) - if (userIndex > -1) { - state.allUsers[userIndex] = responseUser.body.user; - } - - ctx.setState({ - ...state, - isLoading: false, - }); - }, - error: (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - }) - ); + ctx.setState({ + ...state, + isLoading: false + }); + }, + error: (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + }) + ); + } + + @Action(SetUserAction) + setUser(ctx: StateContext, + { user }: SetUserAction + ) { + const state = ctx.getState(); + const userIndex = state.allUsers.findIndex(u => u.id === user.id); + if (userIndex > -1) { + state.allUsers[userIndex] = user; } - @Action(SetUserAction) - setUser(ctx: StateContext, - { user }: SetUserAction - ) { - const state = ctx.getState(); - const userIndex = state.allUsers.findIndex(u => u.id === user.id); - if (userIndex > -1) { - state.allUsers[userIndex] = user; - } - - if ((state.user).id === user.id) { - state.user = user; - } - - ctx.setState({ ...state, isLoading: false }); - ctx.dispatch([ - new SetUserProjectGroupsAction(user), - new SetProjectMemberAction(user) - ]); - } - - @Action(RemoveUserAction) - removeUser(ctx: StateContext, - { user }: RemoveUserAction - ) { - const state = ctx.getState(); - state.allUsers.splice(state.allUsers.findIndex(u => u.id === user.id), 1); - - ctx.setState({ ...state, isLoading: false }); + if ((state.user).id === user.id) { + state.user = user; } - @Action(SetUserProjectGroupsAction) - setUserProjectGroupsData(ctx: StateContext, - { user }: SetUserProjectGroupsAction - ) { - let isMemberOfSystemAdminGroup = false; - const userProjectGroups: string[] = []; - - // get permission information: a) is user sysadmin? b) get list of project iri's where user is project admin - const groupsPerProject = user.permissions.groupsPerProject; - - if (groupsPerProject) { - const groupsPerProjectKeys: string[] = Object.keys(groupsPerProject); - - for (const key of groupsPerProjectKeys) { - if (key === Constants.SystemProjectIRI) { - //is sysAdmin - isMemberOfSystemAdminGroup = groupsPerProject[key].indexOf(Constants.SystemAdminGroupIRI) > -1; - } - - if (groupsPerProject[key].indexOf(Constants.ProjectAdminGroupIRI) > -1) { - //projectAdmin - userProjectGroups.push(key); - } - } + ctx.setState({ ...state, isLoading: false }); + ctx.dispatch([ + new SetUserProjectGroupsAction(user), + new SetProjectMemberAction(user) + ]); + } + + @Action(RemoveUserAction) + removeUser(ctx: StateContext, + { user }: RemoveUserAction + ) { + const state = ctx.getState(); + state.allUsers.splice(state.allUsers.findIndex(u => u.id === user.id), 1); + + ctx.setState({ ...state, isLoading: false }); + } + + @Action(SetUserProjectGroupsAction) + setUserProjectGroupsData(ctx: StateContext, + { user }: SetUserProjectGroupsAction + ) { + let isMemberOfSystemAdminGroup = false; + const userProjectGroups: string[] = []; + + // get permission information: a) is user sysadmin? b) get list of project iri's where user is project admin + const groupsPerProject = user.permissions.groupsPerProject; + + if (groupsPerProject) { + const groupsPerProjectKeys: string[] = Object.keys(groupsPerProject); + + for (const key of groupsPerProjectKeys) { + if (key === Constants.SystemProjectIRI) { + //is sysAdmin + isMemberOfSystemAdminGroup = groupsPerProject[key].indexOf(Constants.SystemAdminGroupIRI) > -1; } - const state = ctx.getState(); - if (state.user?.username === user.username) { - state.userProjectAdminGroups = userProjectGroups; - state.isMemberOfSystemAdminGroup = isMemberOfSystemAdminGroup; - } - - ctx.setState({ ...state }); - } - - @Action(LogUserOutAction) - logUserOut(ctx: StateContext) { - return of(ctx.getState()).pipe( - map(currentState => { - ctx.setState(defaults); - return currentState; - }) - ); - } - - @Action(LoadUsersAction) - loadUsersAction( - ctx: StateContext, - { loadFullUserData }: LoadUsersAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.usersEndpoint.getUsers() - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - ctx.setState({ - ...ctx.getState(), - allUsers: response.body.users - }); - - if (loadFullUserData) { - response.body.users.map(u => ctx.dispatch(new LoadUserContentByIriAction(u.id))); - } - }, - error: (error) => { - this._errorHandler.showMessage(error); - } - }) - ); + if (groupsPerProject[key].indexOf(Constants.ProjectAdminGroupIRI) > -1) { + //projectAdmin + userProjectGroups.push(key); + } + } } - @Action(ResetUsersAction) - resetUsers(ctx: StateContext) { - ctx.patchState({ allUsers: defaults.allUsers }); + const state = ctx.getState(); + if (state.user?.username === user.username) { + state.userProjectAdminGroups = userProjectGroups; + state.isMemberOfSystemAdminGroup = isMemberOfSystemAdminGroup; } - @Action(CreateUserAction) - createUserAction( - ctx: StateContext, - { userData }: CreateUserAction - ) { - ctx.patchState({ isLoading: true }); - return this._dspApiConnection.admin.usersEndpoint.createUser(userData) - .pipe( - take(1), - map((response: ApiResponseData | ApiResponseError) => { - return response as ApiResponseData; - }), - tap({ - next: (response: ApiResponseData) => { - const state = ctx.getState() - state.allUsers.push(response.body.user); - state.isLoading = false; - ctx.patchState(state); - }, - error: (error) => { - this._errorHandler.showMessage(error); - } - }) - ); - } + ctx.setState({ ...state }); + } + + @Action(LogUserOutAction) + logUserOut(ctx: StateContext) { + return of(ctx.getState()).pipe( + map(currentState => { + ctx.setState(defaults); + return currentState; + }) + ); + } + + @Action(LoadUsersAction) + loadUsersAction( + ctx: StateContext, + { loadFullUserData }: LoadUsersAction + ) { + ctx.patchState({ isLoading: true }); + return this._userApiService.list() + .pipe( + take(1), + tap({ + next: response => { + ctx.setState({ + ...ctx.getState(), + allUsers: response.users + }); + + if (loadFullUserData) { + response.users.map(u => ctx.dispatch(new LoadUserContentByIriAction(u.id))); + } + }, + error: (error) => { + this._errorHandler.showMessage(error); + } + }) + ); + } + + @Action(ResetUsersAction) + resetUsers(ctx: StateContext) { + ctx.patchState({ allUsers: defaults.allUsers }); + } + + @Action(CreateUserAction) + createUserAction( + ctx: StateContext, + { userData }: CreateUserAction + ) { + ctx.patchState({ isLoading: true }); + return this._userApiService.create(userData) + .pipe( + take(1), + tap({ + next: response => { + const state = ctx.getState(); + state.allUsers.push(response.user); + state.isLoading = false; + ctx.patchState(state); + }, + error: (error) => { + this._errorHandler.showMessage(error); + } + }) + ); + } } diff --git a/tsconfig.base.json b/tsconfig.base.json index eaba1de7bc..cd147473bc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,6 +31,9 @@ "@dasch-swiss/vre/shared/app-analytics": [ "libs/vre/shared/app-analytics/src/index.ts" ], + "@dasch-swiss/vre/shared/app-api": [ + "libs/vre/shared/app-api/src/index.ts" + ], "@dasch-swiss/vre/shared/app-config": [ "libs/vre/shared/app-config/src/index.ts" ], @@ -58,12 +61,12 @@ "@dasch-swiss/vre/shared/app-state-service": [ "libs/vre/shared/app-state-service/src/index.ts" ], - "@dasch-swiss/vre/shared/assets/status-msg": [ - "libs/vre/shared/assets/status-msg/src/index.ts" - ], "@dasch-swiss/vre/shared/app-string-literal": [ "libs/vre/shared/app-string-literal/src/index.ts" ], + "@dasch-swiss/vre/shared/assets/status-msg": [ + "libs/vre/shared/assets/status-msg/src/index.ts" + ], "@dasch-swiss/vre/shared/app-helper-services": [ "libs/vre/shared/app-helper-services/src/index.ts" ],