From 8060756d4b13abdcd26f822f4caec25702c88466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kilchenmann?= Date: Thu, 12 Aug 2021 12:43:28 +0200 Subject: [PATCH] feat(resource): create link / collection resource (DSP-1835) (#501) --- package-lock.json | 64 ++-- package.json | 2 +- src/app/app.module.ts | 2 + src/app/main/dialog/dialog.component.html | 7 + src/app/main/dialog/dialog.component.ts | 2 + .../intermediate/intermediate.component.html | 23 +- .../intermediate.component.spec.ts | 49 ++- .../intermediate/intermediate.component.ts | 41 ++- .../resource/project.service.spec.ts | 41 +++ src/app/workspace/resource/project.service.ts | 65 ++++ .../resource-instance-form.component.spec.ts | 21 +- .../resource-instance-form.component.ts | 82 ++--- .../select-project.component.ts | 3 +- .../resource-link-form.component.html | 56 ++++ .../resource-link-form.component.scss | 0 .../resource-link-form.component.spec.ts | 279 ++++++++++++++++++ .../resource-link-form.component.ts | 164 ++++++++++ .../workspace/results/results.component.html | 14 +- .../workspace/results/results.component.ts | 31 +- 19 files changed, 772 insertions(+), 174 deletions(-) create mode 100644 src/app/workspace/resource/project.service.spec.ts create mode 100644 src/app/workspace/resource/project.service.ts create mode 100644 src/app/workspace/resource/resource-link-form/resource-link-form.component.html create mode 100644 src/app/workspace/resource/resource-link-form/resource-link-form.component.scss create mode 100644 src/app/workspace/resource/resource-link-form/resource-link-form.component.spec.ts create mode 100644 src/app/workspace/resource/resource-link-form/resource-link-form.component.ts diff --git a/package-lock.json b/package-lock.json index dabf8a78f7..515766f31b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@angular/router": "^11.2.9", "@ckeditor/ckeditor5-angular": "^1.2.3", "@dasch-swiss/dsp-js": "^3.0.0", - "@dasch-swiss/dsp-ui": "^1.7.4", + "@dasch-swiss/dsp-ui": "^1.7.5", "@ngx-translate/core": "^12.1.2", "@ngx-translate/http-loader": "5.0.0", "3d-force-graph": "^1.60.12", @@ -76,28 +76,6 @@ "typescript": "4.0.7" } }, - ".yalc/@dasch-swiss/dsp-ui": { - "version": "1.7.1", - "extraneous": true, - "license": "AGPL-3.0-or-later", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@angular/cdk": "^11.2.5", - "@angular/common": "~11.2.6", - "@angular/core": "~11.2.6", - "@angular/material": "^11.2.5", - "@ckeditor/ckeditor5-angular": "^1.2.3", - "@dasch-swiss/dsp-js": "^2.7.0", - "ckeditor5-custom-build": "github:dasch-swiss/ckeditor_custom_build", - "jdnconvertiblecalendar": "^0.0.7", - "jdnconvertiblecalendardateadapter": "^0.0.17", - "ngx-color-picker": "^11.0.0", - "openseadragon": "^2.4.2", - "svg-overlay": "github:openseadragon/svg-overlay" - } - }, "node_modules/@angular-devkit/architect": { "version": "0.1102.14", "dev": true, @@ -2137,9 +2115,9 @@ } }, "node_modules/@dasch-swiss/dsp-ui": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.7.4.tgz", - "integrity": "sha512-fQyHcgl8w68T3XcvamR0jhcTiUqo3b+VB+uwJHWP6pjHx4uF9DQCSKHtDVmBkmAbCo+99bD3I4hDROrvNbq/TQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.7.5.tgz", + "integrity": "sha512-SeJ4sBlhtH+lQVrhucsJdik4XI9Ek7u20j5OmyjDxG6ekoZFXmdhGcn1BmS5VjJAo48H67z3L+7kj05Za8s94Q==", "dependencies": { "tslib": "^2.0.0" }, @@ -2150,6 +2128,7 @@ "@angular/material": "^11.2.5", "@ckeditor/ckeditor5-angular": "^1.2.3", "@dasch-swiss/dsp-js": "^3.0.0", + "angular-split": "^4.0.0", "ckeditor5-custom-build": "github:dasch-swiss/ckeditor_custom_build", "jdnconvertiblecalendar": "^0.0.7", "jdnconvertiblecalendardateadapter": "^0.0.17", @@ -9928,9 +9907,10 @@ } }, "node_modules/jszip": { - "version": "3.6.0", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "dev": true, - "license": "(MIT OR GPL-3.0)", "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -15867,9 +15847,10 @@ } }, "node_modules/tar": { - "version": "6.1.0", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", + "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", "dev": true, - "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -16548,9 +16529,10 @@ } }, "node_modules/url-parse": { - "version": "1.5.1", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "dev": true, - "license": "MIT", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -20036,9 +20018,9 @@ } }, "@dasch-swiss/dsp-ui": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.7.4.tgz", - "integrity": "sha512-fQyHcgl8w68T3XcvamR0jhcTiUqo3b+VB+uwJHWP6pjHx4uF9DQCSKHtDVmBkmAbCo+99bD3I4hDROrvNbq/TQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.7.5.tgz", + "integrity": "sha512-SeJ4sBlhtH+lQVrhucsJdik4XI9Ek7u20j5OmyjDxG6ekoZFXmdhGcn1BmS5VjJAo48H67z3L+7kj05Za8s94Q==", "requires": { "tslib": "^2.0.0" } @@ -25369,7 +25351,9 @@ } }, "jszip": { - "version": "3.6.0", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "dev": true, "requires": { "lie": "~3.3.0", @@ -29308,7 +29292,9 @@ "dev": true }, "tar": { - "version": "6.1.0", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", + "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", "dev": true, "requires": { "chownr": "^2.0.0", @@ -29765,7 +29751,9 @@ } }, "url-parse": { - "version": "1.5.1", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "dev": true, "requires": { "querystringify": "^2.1.1", diff --git a/package.json b/package.json index f7d903ad59..f9fa8dd9a6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@angular/router": "^11.2.9", "@ckeditor/ckeditor5-angular": "^1.2.3", "@dasch-swiss/dsp-js": "^3.0.0", - "@dasch-swiss/dsp-ui": "^1.7.4", + "@dasch-swiss/dsp-ui": "^1.7.5", "@ngx-translate/core": "^12.1.2", "@ngx-translate/http-loader": "5.0.0", "3d-force-graph": "^1.60.12", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a9e0dcbf83..6969afeda0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -97,6 +97,7 @@ import { ResourceComponent } from './workspace/resource/resource.component'; import { ResultsComponent } from './workspace/results/results.component'; import { AudioComponent } from './workspace/resource/representation/audio/audio.component'; import { IntermediateComponent } from './workspace/intermediate/intermediate.component'; +import { ResourceLinkFormComponent } from './workspace/resource/resource-link-form/resource-link-form.component'; // translate: AoT requires an exported function for factories export function httpLoaderFactory(httpClient: HttpClient) { @@ -180,6 +181,7 @@ export function httpLoaderFactory(httpClient: HttpClient) { VisualizerComponent, AudioComponent, IntermediateComponent, + ResourceLinkFormComponent, ], imports: [ AppRoutingModule, diff --git a/src/app/main/dialog/dialog.component.html b/src/app/main/dialog/dialog.component.html index 3106899217..8e496466c1 100644 --- a/src/app/main/dialog/dialog.component.html +++ b/src/app/main/dialog/dialog.component.html @@ -387,6 +387,13 @@ +
+ + + + +
+
diff --git a/src/app/main/dialog/dialog.component.ts b/src/app/main/dialog/dialog.component.ts index 1dd784b289..a89926f6d7 100644 --- a/src/app/main/dialog/dialog.component.ts +++ b/src/app/main/dialog/dialog.component.ts @@ -1,5 +1,6 @@ import { Component, Inject, OnInit, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FilteredResources } from '@dasch-swiss/dsp-ui'; import { PropertyInfoObject } from 'src/app/project/ontology/default-data/default-properties'; export interface DialogData { @@ -16,6 +17,7 @@ export interface DialogData { position?: number; parentIri?: string; projectCode?: string; + selectedResources?: FilteredResources; } export interface ConfirmationWithComment { diff --git a/src/app/workspace/intermediate/intermediate.component.html b/src/app/workspace/intermediate/intermediate.component.html index 41c3e6293f..1a17dd3fc6 100644 --- a/src/app/workspace/intermediate/intermediate.component.html +++ b/src/app/workspace/intermediate/intermediate.component.html @@ -9,26 +9,15 @@
- - - - - + - diff --git a/src/app/workspace/intermediate/intermediate.component.spec.ts b/src/app/workspace/intermediate/intermediate.component.spec.ts index f1a9dd8f04..5ec766c9e6 100644 --- a/src/app/workspace/intermediate/intermediate.component.spec.ts +++ b/src/app/workspace/intermediate/intermediate.component.spec.ts @@ -1,10 +1,13 @@ import { Component, DebugElement, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { By } from '@angular/platform-browser'; -import { FilteredResouces } from '@dasch-swiss/dsp-ui'; +import { KnoraApiConnection } from '@dasch-swiss/dsp-js'; +import { AppInitService, DspActionModule, DspApiConfigToken, DspApiConnectionToken, FilteredResources } from '@dasch-swiss/dsp-ui'; +import { TestConfig } from 'test.config'; import { IntermediateComponent } from './intermediate.component'; /** @@ -17,12 +20,13 @@ class OneSelectedResourcesComponent { @ViewChild('intermediateView') intermediateComponent: IntermediateComponent; - resources: FilteredResouces = { + resources: FilteredResources = { 'count': 1, 'resListIndex': [1], - 'resIds': [ - 'http://rdfh.ch/0803/83616f8d8501' - ], + 'resInfo': [{ + 'id': 'http://rdfh.ch/0803/83616f8d8501', + 'label': '65r' + }], 'selectionType': 'multiple' }; @@ -40,13 +44,22 @@ class ThreeSelectedResourcesComponent { @ViewChild('intermediateView') intermediateComponent: IntermediateComponent; - resources: FilteredResouces = { + resources: FilteredResources = { 'count': 3, 'resListIndex': [3, 2, 1], - 'resIds': [ - 'http://rdfh.ch/0803/83616f8d8501', - 'http://rdfh.ch/0803/71e0b9958a01', - 'http://rdfh.ch/0803/683d5cd26f01' + 'resInfo': [ + { + 'id': 'http://rdfh.ch/0803/83616f8d8501', + 'label': '65r' + }, + { + 'id': 'http://rdfh.ch/0803/71e0b9958a01', + 'label': '76r' + }, + { + 'id': 'http://rdfh.ch/0803/683d5cd26f01', + 'label': '17v' + }, ], 'selectionType': 'multiple' }; @@ -71,12 +84,24 @@ describe('IntermediateComponent', () => { ThreeSelectedResourcesComponent ], imports: [ + DspActionModule, MatButtonModule, + MatDialogModule, MatIconModule, MatTooltipModule + ], + providers: [ + AppInitService, + { + provide: DspApiConfigToken, + useValue: TestConfig.ApiConfig + }, + { + provide: DspApiConnectionToken, + useValue: new KnoraApiConnection(TestConfig.ApiConfig) + } ] - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/workspace/intermediate/intermediate.component.ts b/src/app/workspace/intermediate/intermediate.component.ts index 195b6ad9d7..1cf66b28b4 100644 --- a/src/app/workspace/intermediate/intermediate.component.ts +++ b/src/app/workspace/intermediate/intermediate.component.ts @@ -1,5 +1,8 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FilteredResouces } from '@dasch-swiss/dsp-ui'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { FilteredResources } from '@dasch-swiss/dsp-ui'; +import { DialogComponent } from 'src/app/main/dialog/dialog.component'; +import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; @Component({ selector: 'app-intermediate', @@ -8,7 +11,7 @@ import { FilteredResouces } from '@dasch-swiss/dsp-ui'; }) export class IntermediateComponent implements OnInit { - @Input() resources: FilteredResouces; + @Input() resources: FilteredResources; @Output() action: EventEmitter = new EventEmitter(); @@ -20,8 +23,40 @@ export class IntermediateComponent implements OnInit { } }; - constructor() { } + constructor( + private _dialog: MatDialog, + private _errorHandler: ErrorHandlerService, + ) { } ngOnInit(): void { } + /** + * opens the dialog box with a form to create a link resource, to edit resources etc. + * @param type 'link' --> TODO: will be expanded with other types like edit, delete etc. + * @param data + */ + openDialog(type: 'link', data: FilteredResources) { + + const title = 'Create a collection of ' + data.count + ' resources'; + + const dialogConfig: MatDialogConfig = { + width: '640px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { mode: type + 'Resources', title: title, selectedResources: data } + }; + + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe((resId: string) => { + + // do something with the intermediate view... but what should we do / display? Maybe the new resource... + }); + } + } diff --git a/src/app/workspace/resource/project.service.spec.ts b/src/app/workspace/resource/project.service.spec.ts new file mode 100644 index 0000000000..ca524ee8ed --- /dev/null +++ b/src/app/workspace/resource/project.service.spec.ts @@ -0,0 +1,41 @@ +import { TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DspActionModule, DspApiConnectionToken, DspCoreModule } from '@dasch-swiss/dsp-ui'; +import { ProjectService } from './project.service'; + + +describe('ProjectService', () => { + let service: ProjectService; + + beforeEach(() => { + + const apiEndpointSpyObj = { + v2: { + auth: jasmine.createSpyObj('auth', ['logout']) + } + }; + + TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + DspActionModule, + DspCoreModule, + MatDialogModule, + MatSnackBarModule + ], + providers: [ + { + provide: DspApiConnectionToken, + useValue: apiEndpointSpyObj + }, + ] + }); + service = TestBed.inject(ProjectService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/workspace/resource/project.service.ts b/src/app/workspace/resource/project.service.ts new file mode 100644 index 0000000000..75993b872c --- /dev/null +++ b/src/app/workspace/resource/project.service.ts @@ -0,0 +1,65 @@ +import { Inject, Injectable } from '@angular/core'; +import { ApiResponseData, ApiResponseError, Constants, KnoraApiConnection, ProjectsResponse, StoredProject, UserResponse } from '@dasch-swiss/dsp-js'; +import { DspApiConnectionToken, SessionService } from '@dasch-swiss/dsp-ui'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { CacheService } from 'src/app/main/cache/cache.service'; +import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ProjectService { + + constructor( + @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, + private _cache: CacheService, + private _errorHandler: ErrorHandlerService, + private _session: SessionService + ) { } + + /** + * initializes projects + * @returns projects + */ + initializeProjects(): Observable { + const usersProjects: StoredProject[] = []; + + // get info about logged-in user from the session object + const session = this._session.getSession(); + + if (session.user.sysAdmin === false) { + return this._cache.get(session.user.name, this._dspApiConnection.admin.usersEndpoint.getUserByUsername(session.user.name)).pipe( + map((response: ApiResponseData) => { + + for (const project of response.body.user.projects) { + if (project.status) { + usersProjects.push(project); + } + } + return usersProjects; + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + return []; + }) + ); + } else { + return this._dspApiConnection.admin.projectsEndpoint.getProjects().pipe( + map((response: ApiResponseData) => { + for (const project of response.body.projects) { + if (project.status && project.id !== Constants.SystemProjectIRI && project.id !== Constants.DefaultSharedOntologyIRI) { + usersProjects.push(project); + } + } + return usersProjects; + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + return []; + }) + ); + } + + } +} diff --git a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts index 09dd2c7578..5f391254ab 100644 --- a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts +++ b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts @@ -35,13 +35,19 @@ import { UsersEndpointAdmin } from '@dasch-swiss/dsp-js'; import { OntologyCache } from '@dasch-swiss/dsp-js/src/cache/ontology-cache/OntologyCache'; -import { DspActionModule, DspApiConnectionToken, IntValueComponent, Session, SessionService, ValueService } from '@dasch-swiss/dsp-ui'; +import { + DspActionModule, + DspApiConnectionToken, + IntValueComponent, + Session, + SessionService, + ValueService +} from '@dasch-swiss/dsp-ui'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; import { AjaxResponse } from 'rxjs/ajax'; import { CacheService } from 'src/app/main/cache/cache.service'; import { BaseValueDirective } from 'src/app/main/directive/base-value.directive'; -import { ResourceComponent } from '../resource.component'; import { ResourceInstanceFormComponent } from './resource-instance-form.component'; import { SwitchPropertiesComponent } from './select-properties/switch-properties/switch-properties.component'; @@ -252,7 +258,7 @@ describe('ResourceInstanceFormComponent', () => { const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']); - const routerSpy = jasmine.createSpyObj('Router', ['navigate', 'navigateByUrl']); + // const routerSpy = jasmine.createSpyObj('Router', ['navigate', 'navigateByUrl']); TestBed.configureTestingModule({ declarations: [ @@ -275,9 +281,7 @@ describe('ResourceInstanceFormComponent', () => { MatSelectModule, MatSnackBarModule, ReactiveFormsModule, - RouterTestingModule.withRoutes([ - { path: 'resource', component: ResourceComponent } - ]), + RouterTestingModule, TranslateModule.forRoot() ], providers: [ @@ -341,11 +345,6 @@ describe('ResourceInstanceFormComponent', () => { } ); - // const routerSpy = TestBed.inject(Router); - - // (routerSpy as jasmine.SpyObj).navigate.and.stub(); - // (routerSpy as jasmine.SpyObj).navigateByUrl.and.stub(); - testHostFixture = TestBed.createComponent(TestHostComponent); testHostComponent = testHostFixture.componentInstance; loader = TestbedHarnessEnvironment.loader(testHostFixture); diff --git a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts index 59a0aea139..10e418c007 100644 --- a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts +++ b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts @@ -2,31 +2,25 @@ import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output, ViewChild } import { FormBuilder, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { - ApiResponseData, ApiResponseError, Constants, CreateFileValue, CreateResource, CreateValue, KnoraApiConnection, - OntologiesMetadata, - ProjectsResponse, - ReadOntology, + OntologiesMetadata, ReadOntology, ReadResource, ResourceClassAndPropertyDefinitions, ResourceClassDefinition, ResourcePropertyDefinition, - StoredProject, - UserResponse + StoredProject } from '@dasch-swiss/dsp-js'; import { - DspApiConnectionToken, - Session, - SessionService + DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; import { Subscription } from 'rxjs'; -import { CacheService } from 'src/app/main/cache/cache.service'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { ProjectService } from '../project.service'; import { SelectOntologyComponent } from './select-ontology/select-ontology.component'; import { SelectPropertiesComponent } from './select-properties/select-properties.component'; import { SelectResourceClassComponent } from './select-resource-class/select-resource-class.component'; @@ -57,9 +51,6 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy { // form validation status formValid = false; - session: Session; - username: string; - showNextStepForm: boolean; usersProjects: StoredProject[]; @@ -86,15 +77,11 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _cache: CacheService, private _errorHandler: ErrorHandlerService, private _fb: FormBuilder, - private _router: Router, - private _session: SessionService - ) { - this.session = this._session.getSession(); - this.username = this.session.user.name; - } + private _project: ProjectService, + private _router: Router + ) { } ngOnInit(): void { @@ -104,7 +91,16 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy { this.propertiesParentForm = this._fb.group({}); // initialize projects to be used for the project selection in the creation form - this.initializeProjects(); + this._project.initializeProjects().subscribe( + (proj: StoredProject[]) => { + this.usersProjects = proj; + + // notifies the user that he/she is not part of any project + if (proj.length === 0) { + this.errorMessage = 'You are not a part of any active projects or something went wrong'; + } + } + ); // boolean to show only the first step of the form (= selectResourceForm) this.showNextStepForm = true; @@ -185,7 +181,7 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy { this.resource = res; const goto = '/resource/' + encodeURIComponent(this.resource.id); - this._router.navigateByUrl(goto, { skipLocationChange: false }); + this._router.navigate([]).then(result => window.open(goto, '_blank')); this.closeDialog.emit(); }, @@ -199,48 +195,6 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy { } } - /** - * get the user's project(s) - */ - initializeProjects(): void { - this.usersProjects = []; - - if (this.username && this.session.user.sysAdmin === false) { - this._cache.get(this.username, this._dspApiConnection.admin.usersEndpoint.getUserByUsername(this.username)).subscribe( - (response: ApiResponseData) => { - - for (const project of response.body.user.projects) { - if (project.status) { - this.usersProjects.push(project); - } - } - - // notifies the user that he/she is not part of any project - if (this.usersProjects.length === 0) { - this.errorMessage = 'You are not a part of any active projects.'; - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } else if (this.session.user.sysAdmin === true) { - this._dspApiConnection.admin.projectsEndpoint.getProjects().subscribe( - (response: ApiResponseData) => { - for (const project of response.body.projects) { - if (project.status && project.id !== Constants.SystemProjectIRI && project.id !== Constants.DefaultSharedOntologyIRI) { - this.usersProjects.push(project); - } - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - } - - } - /** * get all the ontologies of the selected project * @param projectIri diff --git a/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts b/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts index 4ec0f4c1c0..458e3be1bc 100644 --- a/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts +++ b/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts @@ -9,8 +9,7 @@ import { Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { KnoraApiConnection, StoredProject } from '@dasch-swiss/dsp-js'; -import { DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; +import { StoredProject } from '@dasch-swiss/dsp-js'; import { Subscription } from 'rxjs'; const resolvedPromise = Promise.resolve(null); diff --git a/src/app/workspace/resource/resource-link-form/resource-link-form.component.html b/src/app/workspace/resource/resource-link-form/resource-link-form.component.html new file mode 100644 index 0000000000..37ea351862 --- /dev/null +++ b/src/app/workspace/resource/resource-link-form/resource-link-form.component.html @@ -0,0 +1,56 @@ +
+ +
+ + + + + + + + {{ formErrors.label }} + + + + + + + +
+

The following resources will be connected:

+
    +
  • {{res.label}}
  • +
+
+ +
+ + + + + + + +
+
+
+ + + You have to be a member in at least one project to link the selected resources. + diff --git a/src/app/workspace/resource/resource-link-form/resource-link-form.component.scss b/src/app/workspace/resource/resource-link-form/resource-link-form.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/workspace/resource/resource-link-form/resource-link-form.component.spec.ts b/src/app/workspace/resource/resource-link-form/resource-link-form.component.spec.ts new file mode 100644 index 0000000000..d830de8fd0 --- /dev/null +++ b/src/app/workspace/resource/resource-link-form/resource-link-form.component.spec.ts @@ -0,0 +1,279 @@ +import { Component, DebugElement, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { + ApiResponseData, + Constants, + CreateLinkValue, + CreateResource, + MockProjects, + MockResource, + MockUsers, + ReadResource, ResourcesEndpointV2, + StoredProject, + UserResponse, + UsersEndpointAdmin +} from '@dasch-swiss/dsp-js'; +import { + DspActionModule, + DspApiConnectionToken, + FilteredResources, + Session, + SessionService +} from '@dasch-swiss/dsp-ui'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { AjaxResponse } from 'rxjs/ajax'; +import { CacheService } from 'src/app/main/cache/cache.service'; +import { ResourceLinkFormComponent } from './resource-link-form.component'; + +const resolvedPromise = Promise.resolve(null); + +/** + * test host component to simulate parent component. + */ +@Component({ + template: ` + ` +}) +class TestHostComponent implements OnInit { + + @ViewChild('resourceLinkFormComp') resourceLinkFormComponent: ResourceLinkFormComponent; + + resources: FilteredResources = { + 'count': 3, + 'resListIndex': [3, 2, 1], + 'resInfo': [ + { + 'id': 'http://rdfh.ch/0803/83616f8d8501', + 'label': '65r' + }, + { + 'id': 'http://rdfh.ch/0803/71e0b9958a01', + 'label': '76r' + }, + { + 'id': 'http://rdfh.ch/0803/683d5cd26f01', + 'label': '17v' + }, + ], + 'selectionType': 'multiple' + }; + + constructor() { } + + ngOnInit() { + } + +} + +/** + * mock select-project component to use in tests. + */ +@Component({ + selector: 'app-select-project' +}) +class MockSelectProjectComponent implements OnInit { + @Input() formGroup: FormGroup; + @Input() usersProjects: StoredProject[]; + @Output() projectSelected = new EventEmitter(); + + form: FormGroup; + + constructor(@Inject(FormBuilder) private _fb: FormBuilder) { } + + ngOnInit() { + this.form = this._fb.group({ + projects: [null, Validators.required] + }); + + resolvedPromise.then(() => { + // add form to the parent form group + this.formGroup.addControl('projects', this.form); + }); + } +} + +describe('ResourceLinkFormComponent', () => { + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; + let resourceLinkFormComponentDe: DebugElement; + + beforeEach(waitForAsync(() => { + const dspConnSpy = { + admin: { + usersEndpoint: jasmine.createSpyObj('usersEndpoint', ['getUserByUsername']) + }, + v2: { + onto: jasmine.createSpyObj('onto', ['getOntologiesByProjectIri']), + ontologyCache: jasmine.createSpyObj('ontologyCache', ['getOntology', 'getResourceClassDefinition']), + res: jasmine.createSpyObj('res', ['createResource']) + } + }; + + const sessionServiceSpy = jasmine.createSpyObj('SessionService', ['getSession']); + + const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']); + + TestBed.configureTestingModule({ + declarations: [ + ResourceLinkFormComponent, + TestHostComponent, + MockSelectProjectComponent + ], + imports: [ + BrowserAnimationsModule, + DspActionModule, + MatButtonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatSelectModule, + MatSnackBarModule, + MatTooltipModule, + ReactiveFormsModule, + RouterTestingModule, + TranslateModule.forRoot() + ], + providers: [ + { + provide: DspApiConnectionToken, + useValue: dspConnSpy + }, + { + provide: SessionService, + useValue: sessionServiceSpy + }, + { + provide: CacheService, + useValue: cacheServiceSpy + } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + + const sessionSpy = TestBed.inject(SessionService); + + (sessionSpy as jasmine.SpyObj).getSession.and.callFake( + () => { + const session: Session = { + id: 12345, + user: { + name: 'username', + jwt: 'myToken', + lang: 'en', + sysAdmin: false, + projectAdmin: [] + } + }; + + return session; + } + ); + + const cacheSpy = TestBed.inject(CacheService); + + (cacheSpy as jasmine.SpyObj).get.and.callFake( + () => { + const response: UserResponse = new UserResponse(); + + const project = MockProjects.mockProject(); + + response.user.projects = new Array(); + + response.user.projects.push(project.body.project); + + return of(ApiResponseData.fromAjaxResponse({ response } as AjaxResponse)); + } + ); + + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + const dspConnSpy = TestBed.inject(DspApiConnectionToken); + + (dspConnSpy.admin.usersEndpoint as jasmine.SpyObj).getUserByUsername.and.callFake( + () => { + const loggedInUser = MockUsers.mockUser(); + return of(loggedInUser); + } + ); + + const hostCompDe = testHostFixture.debugElement; + + resourceLinkFormComponentDe = hostCompDe.query(By.directive(ResourceLinkFormComponent)); + + }); + + + it('should initialize the usersProjects array', () => { + expect(testHostComponent.resourceLinkFormComponent.usersProjects.length).toEqual(1); + }); + + + it('should show the select project component', () => { + + const comp = resourceLinkFormComponentDe.query(By.directive(MockSelectProjectComponent)); + + expect((comp.componentInstance as MockSelectProjectComponent).usersProjects.length).toEqual(1); + }); + + it('should submit the form', () => { + const dspConnSpy = TestBed.inject(DspApiConnectionToken); + + (dspConnSpy.v2.res as jasmine.SpyObj).createResource.and.callFake( + () => { + let resource = new ReadResource(); + + MockResource.getTestThing().subscribe((res) => { + resource = res; + }); + + return of(resource); + } + ); + + testHostComponent.resourceLinkFormComponent.form.controls['label'].setValue('My Label'); + + testHostFixture.detectChanges(); + + const props = {}; + const createVal: CreateLinkValue[] = []; + // res 1 + testHostComponent.resources.resInfo.forEach(res => { + const linkVal = new CreateLinkValue(); + linkVal.linkedResourceIri = res.id; + linkVal.type = Constants.LinkValue; + createVal.push(linkVal); + }); + + props[Constants.KnoraApiV2 + Constants.HashDelimiter + 'hasLinkToValue'] = createVal; + + const expectedCreateResource = new CreateResource(); + expectedCreateResource.label = 'My Label'; + expectedCreateResource.type = Constants.KnoraApiV2 + Constants.HashDelimiter + 'LinkObj'; + expectedCreateResource.properties = props; + + // --> TODO create a Router spy to mock the navigation + testHostComponent.resourceLinkFormComponent.submitData(); + + expect(dspConnSpy.v2.res.createResource).toHaveBeenCalledTimes(1); + + + }); + +}); diff --git a/src/app/workspace/resource/resource-link-form/resource-link-form.component.ts b/src/app/workspace/resource/resource-link-form/resource-link-form.component.ts new file mode 100644 index 0000000000..b598ff4a18 --- /dev/null +++ b/src/app/workspace/resource/resource-link-form/resource-link-form.component.ts @@ -0,0 +1,164 @@ +import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { + ApiResponseError, + Constants, + CreateLinkValue, + CreateResource, + CreateTextValueAsString, + KnoraApiConnection, + ReadResource, + StoredProject +} from '@dasch-swiss/dsp-js'; +import { DspApiConnectionToken, FilteredResources } from '@dasch-swiss/dsp-ui'; +import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { ProjectService } from '../project.service'; + +@Component({ + selector: 'app-resource-link-form', + templateUrl: './resource-link-form.component.html', + styleUrls: ['./resource-link-form.component.scss'] +}) +export class ResourceLinkFormComponent implements OnInit { + + @Input() resources: FilteredResources; + + @Output() closeDialog: EventEmitter = new EventEmitter(); + + /** + * form group, errors and validation messages + */ + form: FormGroup; + + formErrors = { + 'label': '' + }; + + validationMessages = { + 'label': { + 'required': 'A label is required.' + } + }; + + usersProjects: StoredProject[]; + + selectedProject: string; + + error = false; + loading = false; + + constructor( + @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, + private _errorHandler: ErrorHandlerService, + private _fb: FormBuilder, + private _project: ProjectService, + private _router: Router + ) { } + + ngOnInit(): void { + + // initialize projects to be used for the project selection in the creation form + this._project.initializeProjects().subscribe( + (proj: StoredProject[]) => { + this.usersProjects = proj; + } + ); + + this.form = this._fb.group({ + 'label': new FormControl({ + value: '', disabled: false + }, [ + Validators.required + ]), + 'comment': new FormControl(), + 'project': new FormControl() + }); + + this.form.valueChanges + .subscribe(data => this.onValueChanged(data)); + } + + /** + * this method is for the form error handling + * + * @param data Data which changed. + */ + onValueChanged(data?: any) { + + if (!this.form) { + return; + } + + const form = this.form; + + 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] + ' '; + }); + + } + }); + } + + /** + * submits the data + */ + submitData() { + + this.loading = true; + + // build link resource as type CreateResource + const linkObj = new CreateResource(); + + linkObj.label = this.form.controls['label'].value; + + linkObj.type = Constants.KnoraApiV2 + Constants.HashDelimiter + 'LinkObj'; + + linkObj.attachedToProject = this.selectedProject; + + const hasLinkToValue = []; + + this.resources.resInfo.forEach(res => { + const linkVal = new CreateLinkValue(); + linkVal.type = Constants.LinkValue; + linkVal.linkedResourceIri = res.id; + hasLinkToValue.push(linkVal); + }); + + const comment = this.form.controls['comment'].value; + if (comment) { + const commentVal = new CreateTextValueAsString(); + commentVal.type = Constants.TextValue; + commentVal.text = comment; + linkObj.properties = { + [Constants.KnoraApiV2 + Constants.HashDelimiter + 'hasLinkToValue']: hasLinkToValue, + [Constants.KnoraApiV2 + Constants.HashDelimiter + 'hasComment']: [commentVal], + }; + } else { + linkObj.properties = { + [Constants.KnoraApiV2 + Constants.HashDelimiter + 'hasLinkToValue']: hasLinkToValue, + }; + } + + this._dspApiConnection.v2.res.createResource(linkObj).subscribe( + (res: ReadResource) => { + // --> TODO: do something with the successful response + const goto = '/resource/' + encodeURIComponent(res.id); + this._router.navigate([]).then(result => window.open(goto, '_blank')); + this.closeDialog.emit(); + this.loading = false; + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + this.error = true; + this.loading = false; + } + ); + + } +} diff --git a/src/app/workspace/results/results.component.html b/src/app/workspace/results/results.component.html index 1d8d66dc90..d81152f757 100644 --- a/src/app/workspace/results/results.component.html +++ b/src/app/workspace/results/results.component.html @@ -4,22 +4,20 @@ + (selectedResources)="openSelectedResources($event)"> - - +
- + - + - +
diff --git a/src/app/workspace/results/results.component.ts b/src/app/workspace/results/results.component.ts index 933d053472..bf2c784de3 100644 --- a/src/app/workspace/results/results.component.ts +++ b/src/app/workspace/results/results.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Params } from '@angular/router'; -import { FilteredResouces, SearchParams } from '@dasch-swiss/dsp-ui'; +import { FilteredResources, SearchParams } from '@dasch-swiss/dsp-ui'; @Component({ selector: 'app-results', @@ -17,16 +17,17 @@ export class ResultsComponent { resourceIri: string; // display single resource or intermediate page in case of multiple selection - multipleResources: FilteredResouces; viewMode: 'single' | 'intermediate' | 'compare' = 'single'; - // number of all results - numberOfAllResults: number; + // which resources are selected? + selectedResources: FilteredResources; // search params searchQuery: string; searchMode: 'fulltext' | 'gravsearch'; + loading = true; + constructor( private _route: ActivatedRoute, private _titleService: Title @@ -53,24 +54,18 @@ export class ResultsComponent { this._titleService.setTitle('Search results for ' + this.searchParams.mode + ' search'); } - openResource(id: string) { - this.viewMode = 'single'; - this.multipleResources = undefined; - this.resourceIri = id; - } - - // this funtion is called when 'withMultipleSelection' is true and - // multiple resources are selected for comparision - openMultipleResources(resources: FilteredResouces) { + openSelectedResources(res: FilteredResources) { - if (this.viewMode !== 'compare') { + this.selectedResources = res; - this.viewMode = ((resources && resources.count > 0) ? 'intermediate' : 'single'); - - this.multipleResources = (this.viewMode !== 'single' ? resources : undefined); + if (!res || res.count <= 1) { + this.viewMode = 'single'; + } else { + if (this.viewMode !== 'compare') { + this.viewMode = ((res && res.count > 0) ? 'intermediate' : 'single'); + } } - } }