From df9de8c5781e11fb2d8dfbcbcd011cbc7bbd4bc5 Mon Sep 17 00:00:00 2001 From: mdelez <60604010+mdelez@users.noreply.github.com> Date: Mon, 4 Apr 2022 09:23:59 +0200 Subject: [PATCH] feat(replace-file): Replace Uploaded Files (DEV-684) (#695) * feat(replace-file): add support for replacing an image file * feat(replace-file): reload UI to show new image * chore: remove console logs * feat(replace-file): add support for replacing an archive file * feat(replace-file): add support for replacing an audio file * feat(replace-file): add support for replacing a document file * style(replace-file): add css to style warning message * feat(replace-file): generate warning messages dynamically * chore(mkdocs): bump version to 1.3.0 and add generated docs to gitignore * test(replace-file): fix tests * chore(replace-file): cleanup --- .gitignore | 3 + src/app/app.module.ts | 2 + src/app/main/dialog/dialog.component.html | 7 ++ src/app/main/dialog/dialog.component.ts | 1 + .../archive/archive.component.html | 3 + .../archive/archive.component.ts | 92 ++++++++++++++++--- .../representation/audio/audio.component.html | 17 +++- .../representation/audio/audio.component.scss | 17 +++- .../representation/audio/audio.component.ts | 71 +++++++++++++- .../document/document.component.html | 3 + .../document/document.component.ts | 69 +++++++++++++- .../replace-file-form.component.html | 27 ++++++ .../replace-file-form.component.scss | 29 ++++++ .../replace-file-form.component.spec.ts | 71 ++++++++++++++ .../replace-file-form.component.ts | 73 +++++++++++++++ .../still-image/still-image.component.html | 3 + .../still-image/still-image.component.spec.ts | 42 ++++++++- .../still-image/still-image.component.ts | 58 +++++++++++- .../resource/resource.component.html | 7 +- .../workspace/resource/resource.component.ts | 18 +++- .../services/value-operation-event.service.ts | 11 ++- 21 files changed, 589 insertions(+), 35 deletions(-) create mode 100644 src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.html create mode 100644 src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.scss create mode 100644 src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.spec.ts create mode 100644 src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.ts diff --git a/.gitignore b/.gitignore index 492ce54460..88635cf051 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ docs/bin .DS_Store Thumbs.db *.code-workspace + +# Generated docs +/site diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 513d783b15..f3e4ed6b91 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -153,6 +153,7 @@ import { SearchSelectOntologyComponent } from './workspace/search/advanced-searc import { ExpertSearchComponent } from './workspace/search/expert-search/expert-search.component'; import { FulltextSearchComponent } from './workspace/search/fulltext-search/fulltext-search.component'; import { SearchPanelComponent } from './workspace/search/search-panel/search-panel.component'; +import { ReplaceFileFormComponent } from './workspace/resource/representation/replace-file-form/replace-file-form.component'; // translate: AoT requires an exported function for factories export function httpLoaderFactory(httpClient: HttpClient) { @@ -294,6 +295,7 @@ export function httpLoaderFactory(httpClient: HttpClient) { UserMenuComponent, UsersComponent, UsersListComponent, + ReplaceFileFormComponent, ], imports: [ AngularSplitModule.forRoot(), diff --git a/src/app/main/dialog/dialog.component.html b/src/app/main/dialog/dialog.component.html index ec28359b5f..d403b2c59c 100644 --- a/src/app/main/dialog/dialog.component.html +++ b/src/app/main/dialog/dialog.component.html @@ -392,6 +392,13 @@ +
+ + + + +
+
diff --git a/src/app/main/dialog/dialog.component.ts b/src/app/main/dialog/dialog.component.ts index 8037e4abef..55bec43ad8 100644 --- a/src/app/main/dialog/dialog.component.ts +++ b/src/app/main/dialog/dialog.component.ts @@ -23,6 +23,7 @@ export interface DialogData { resourceClassDefinition?: string; fullSize?: boolean; ontoIri?: string; + representation?: string; // respresentation type (stillImage, audio, etc.) } export interface ConfirmationWithComment { diff --git a/src/app/workspace/resource/representation/archive/archive.component.html b/src/app/workspace/resource/representation/archive/archive.component.html index 90244b746c..54c8f8f173 100644 --- a/src/app/workspace/resource/representation/archive/archive.component.html +++ b/src/app/workspace/resource/representation/archive/archive.component.html @@ -5,6 +5,9 @@ Click to download +
No valid file url found for this resource. diff --git a/src/app/workspace/resource/representation/archive/archive.component.ts b/src/app/workspace/resource/representation/archive/archive.component.ts index b06e3b11cf..7dabe1647e 100644 --- a/src/app/workspace/resource/representation/archive/archive.component.ts +++ b/src/app/workspace/resource/representation/archive/archive.component.ts @@ -1,6 +1,12 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { Constants, UpdateFileValue, UpdateResource, UpdateValue, WriteValueResponse, ReadResource, ApiResponseError, KnoraApiConnection, ReadArchiveFileValue } from '@dasch-swiss/dsp-js'; +import { mergeMap } from 'rxjs/operators'; +import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; +import { DialogComponent } from 'src/app/main/dialog/dialog.component'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { EmitEvent, Events, UpdatedFileEventValue, ValueOperationEventService } from '../../services/value-operation-event.service'; import { FileRepresentation } from '../file-representation'; @Component({ @@ -11,27 +17,21 @@ import { FileRepresentation } from '../file-representation'; export class ArchiveComponent implements OnInit { @Input() src: FileRepresentation; + @Input() parentResource: ReadResource; + originalFilename: string; temp: string; constructor( + @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private readonly _http: HttpClient, - private _errorHandler: ErrorHandlerService + private _dialog: MatDialog, + private _errorHandler: ErrorHandlerService, + private _valueOperationEventService: ValueOperationEventService ) { } ngOnInit(): void { - const requestOptions = { - headers: new HttpHeaders({ 'Content-Type': 'application/json' }), - withCredentials: true - }; - - const pathToJson = this.src.fileValue.fileUrl.substring(0, this.src.fileValue.fileUrl.lastIndexOf('/')) + '/knora.json'; - - this._http.get(pathToJson, requestOptions).subscribe( - res => { - this.originalFilename = res['originalFilename']; - } - ); + this._getOriginalFilename(); } // https://stackoverflow.com/questions/66986983/angular-10-download-file-from-firebase-link-without-opening-into-new-tab @@ -60,4 +60,68 @@ export class ArchiveComponent implements OnInit { e.click(); document.body.removeChild(e); } + + openReplaceFileDialog(){ + const propId = this.parentResource.properties[Constants.HasArchiveFileValue][0].id; + + const dialogConfig: MatDialogConfig = { + width: '800px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { mode: 'replaceFile', title: 'Archive (zip, x-tar, gzip)', subtitle: 'Update the archive file of this resource' , representation: 'archive', id: propId }, + disableClose: true + }; + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe((data) => { + this._replaceFile(data); + }); + } + + private _getOriginalFilename() { + const requestOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), + withCredentials: true + }; + + const pathToJson = this.src.fileValue.fileUrl.substring(0, this.src.fileValue.fileUrl.lastIndexOf('/')) + '/knora.json'; + + this._http.get(pathToJson, requestOptions).subscribe( + res => { + this.originalFilename = res['originalFilename']; + } + ); + } + + private _replaceFile(file: UpdateFileValue) { + const updateRes = new UpdateResource(); + updateRes.id = this.parentResource.id; + updateRes.type = this.parentResource.type; + updateRes.property = Constants.HasArchiveFileValue; + updateRes.value = file; + + this._dspApiConnection.v2.values.updateValue(updateRes as UpdateResource).pipe( + mergeMap((res: WriteValueResponse) => this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid)) + ).subscribe( + (res2: ReadResource) => { + this.src.fileValue.fileUrl = (res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue).fileUrl; + this.src.fileValue.filename = (res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue).filename; + this.src.fileValue.strval = (res2.properties[Constants.HasArchiveFileValue][0] as ReadArchiveFileValue).strval; + + this._getOriginalFilename(); + + this._valueOperationEventService.emit( + new EmitEvent(Events.FileValueUpdated, new UpdatedFileEventValue( + res2.properties[Constants.HasArchiveFileValue][0]))); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } } diff --git a/src/app/workspace/resource/representation/audio/audio.component.html b/src/app/workspace/resource/representation/audio/audio.component.html index 7e4bf049ca..62c4d5846b 100644 --- a/src/app/workspace/resource/representation/audio/audio.component.html +++ b/src/app/workspace/resource/representation/audio/audio.component.html @@ -1,4 +1,13 @@ - +
+
+ +
+
+ +
+
diff --git a/src/app/workspace/resource/representation/audio/audio.component.scss b/src/app/workspace/resource/representation/audio/audio.component.scss index a1acb2ba8e..48609f7c45 100644 --- a/src/app/workspace/resource/representation/audio/audio.component.scss +++ b/src/app/workspace/resource/representation/audio/audio.component.scss @@ -1,3 +1,18 @@ -audio { +.controls { width: 100%; + + .audio-player, + .upload-button { + display: inline-block; + vertical-align: middle; + } + + .audio-player { + width: 80%; + + audio { + width: 100%; + } + } } + diff --git a/src/app/workspace/resource/representation/audio/audio.component.ts b/src/app/workspace/resource/representation/audio/audio.component.ts index bd30950664..6ab4a594aa 100644 --- a/src/app/workspace/resource/representation/audio/audio.component.ts +++ b/src/app/workspace/resource/representation/audio/audio.component.ts @@ -1,5 +1,12 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { UpdateFileValue, UpdateResource, Constants, UpdateValue, WriteValueResponse, ReadResource, ApiResponseError, KnoraApiConnection, ReadAudioFileValue } from '@dasch-swiss/dsp-js'; +import { mergeMap } from 'rxjs/operators'; +import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; +import { DialogComponent } from 'src/app/main/dialog/dialog.component'; +import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { EmitEvent, Events, UpdatedFileEventValue, ValueOperationEventService } from '../../services/value-operation-event.service'; import { FileRepresentation } from '../file-representation'; @@ -11,15 +18,75 @@ import { FileRepresentation } from '../file-representation'; export class AudioComponent implements OnInit { @Input() src: FileRepresentation; + @Input() parentResource: ReadResource; audio: SafeUrl; constructor( - private _sanitizer: DomSanitizer + @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, + private _sanitizer: DomSanitizer, + private _dialog: MatDialog, + private _errorHandler: ErrorHandlerService, + private _valueOperationEventService: ValueOperationEventService ) { } ngOnInit(): void { this.audio = this._sanitizer.bypassSecurityTrustUrl(this.src.fileValue.fileUrl); } + openReplaceFileDialog(){ + const propId = this.parentResource.properties[Constants.HasAudioFileValue][0].id; + + const dialogConfig: MatDialogConfig = { + width: '800px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { mode: 'replaceFile', title: 'Audio', subtitle: 'Update the audio file of this resource' , representation: 'audio', id: propId }, + disableClose: true + }; + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe((data) => { + this._replaceFile(data); + }); + } + + private _replaceFile(file: UpdateFileValue) { + const updateRes = new UpdateResource(); + updateRes.id = this.parentResource.id; + updateRes.type = this.parentResource.type; + updateRes.property = Constants.HasAudioFileValue; + updateRes.value = file; + + this._dspApiConnection.v2.values.updateValue(updateRes as UpdateResource).pipe( + mergeMap((res: WriteValueResponse) => this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid)) + ).subscribe( + (res2: ReadResource) => { + + this.src.fileValue.fileUrl = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).fileUrl; + this.src.fileValue.filename = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).filename; + this.src.fileValue.strval = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).strval; + this.src.fileValue.valueCreationDate = (res2.properties[Constants.HasAudioFileValue][0] as ReadAudioFileValue).valueCreationDate; + + this.audio = this._sanitizer.bypassSecurityTrustUrl(this.src.fileValue.fileUrl); + + this._valueOperationEventService.emit( + new EmitEvent(Events.FileValueUpdated, new UpdatedFileEventValue( + res2.properties[Constants.HasAudioFileValue][0]))); + + const audioElem = document.getElementById('audio'); + (audioElem as HTMLAudioElement).load(); + + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } + } diff --git a/src/app/workspace/resource/representation/document/document.component.html b/src/app/workspace/resource/representation/document/document.component.html index bdc00cae60..df3b6f16a7 100644 --- a/src/app/workspace/resource/representation/document/document.component.html +++ b/src/app/workspace/resource/representation/document/document.component.html @@ -27,6 +27,9 @@ + launch diff --git a/src/app/workspace/resource/representation/document/document.component.ts b/src/app/workspace/resource/representation/document/document.component.ts index b7ceea727b..e16397e872 100644 --- a/src/app/workspace/resource/representation/document/document.component.ts +++ b/src/app/workspace/resource/representation/document/document.component.ts @@ -1,5 +1,12 @@ -import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { ApiResponseError, Constants, KnoraApiConnection, ReadDocumentFileValue, ReadResource, UpdateFileValue, UpdateResource, UpdateValue, WriteValueResponse } from '@dasch-swiss/dsp-js'; import { PdfViewerComponent } from 'ng2-pdf-viewer'; +import { mergeMap } from 'rxjs/operators'; +import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; +import { DialogComponent } from 'src/app/main/dialog/dialog.component'; +import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { EmitEvent, Events, UpdatedFileEventValue, ValueOperationEventService } from '../../services/value-operation-event.service'; import { FileRepresentation } from '../file-representation'; @Component({ @@ -10,6 +17,7 @@ import { FileRepresentation } from '../file-representation'; export class DocumentComponent implements OnInit { @Input() src: FileRepresentation; + @Input() parentResource: ReadResource; @ViewChild(PdfViewerComponent) private _pdfComponent: PdfViewerComponent; @@ -17,7 +25,12 @@ export class DocumentComponent implements OnInit { pdfQuery = ''; - constructor() { } + constructor( + @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, + private _dialog: MatDialog, + private _errorHandler: ErrorHandlerService, + private _valueOperationEventService: ValueOperationEventService + ) { } ngOnInit(): void { @@ -38,4 +51,56 @@ export class DocumentComponent implements OnInit { } } + openReplaceFileDialog(){ + const propId = this.parentResource.properties[Constants.HasDocumentFileValue][0].id; + + const dialogConfig: MatDialogConfig = { + width: '800px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { mode: 'replaceFile', title: 'Document', subtitle: 'Update the document file of this resource' , representation: 'document', id: propId }, + disableClose: true + }; + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe((data) => { + this._replaceFile(data); + }); + } + + private _replaceFile(file: UpdateFileValue) { + const updateRes = new UpdateResource(); + updateRes.id = this.parentResource.id; + updateRes.type = this.parentResource.type; + updateRes.property = Constants.HasDocumentFileValue; + updateRes.value = file; + + this._dspApiConnection.v2.values.updateValue(updateRes as UpdateResource).pipe( + mergeMap((res: WriteValueResponse) => this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid)) + ).subscribe( + (res2: ReadResource) => { + + this.src.fileValue.fileUrl = (res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue).fileUrl; + this.src.fileValue.filename = (res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue).filename; + this.src.fileValue.strval = (res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue).strval; + this.src.fileValue.valueCreationDate = (res2.properties[Constants.HasDocumentFileValue][0] as ReadDocumentFileValue).valueCreationDate; + + this.zoomFactor = 1.0; + this.pdfQuery = ''; + + this._valueOperationEventService.emit( + new EmitEvent(Events.FileValueUpdated, new UpdatedFileEventValue( + res2.properties[Constants.HasDocumentFileValue][0]))); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } + } diff --git a/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.html b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.html new file mode 100644 index 0000000000..243e4f5658 --- /dev/null +++ b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.html @@ -0,0 +1,27 @@ +
+
+
+ warning +
+
+

{{ warningMessages[0] }}

+
+

{{ warningMessages[1] }}

+
+
+
+ + +
+ + + + + + + +
diff --git a/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.scss b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.scss new file mode 100644 index 0000000000..b0487f4b88 --- /dev/null +++ b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.scss @@ -0,0 +1,29 @@ +.warning { + background-color: #FFFBEB; + border-radius: 5px; + + .container { + padding: 2%; + + .icon, + .message { + display: inline-block; + vertical-align: top; + } + + .icon { + color: #FBBF24 + } + + .message { + padding-left: 2%; + color: #92400E; + + p { + margin-block-start: 0em; + margin-block-end: 0em; + } + } + } + +} diff --git a/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.spec.ts b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.spec.ts new file mode 100644 index 0000000000..793328f104 --- /dev/null +++ b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.spec.ts @@ -0,0 +1,71 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { MatIconModule } from '@angular/material/icon'; +import { TranslateModule } from '@ngx-translate/core'; +import { UploadComponent } from '../upload/upload.component'; + +import { ReplaceFileFormComponent } from './replace-file-form.component'; + +@Component({ + selector: 'test-host-component', + template: ` + + ` +}) +class TestHostComponent implements OnInit { + @ViewChild('replaceFileForm') replaceFileFormComp: ReplaceFileFormComponent; + + representation: 'stillImage' | 'movingImage' | 'audio' | 'document' | 'text' | 'archive'; + propId: string; + + ngOnInit(): void { + this.representation = 'stillImage'; + this.propId = 'http://rdfh.ch/0123/yryzB6ROTaGER3F9kMZoUA/values/mEm67WJiSAqaWf572GzA9Q'; + } + +} + +@Component({ + selector: 'app-upload', + template: '' +}) +class TestUploadComponent { + @Input() parentForm?: FormGroup; + + @Input() representation: 'stillImage' | 'movingImage' | 'audio' | 'document' | 'text' | 'archive'; + + @Input() formName: string; +} + +describe('ReplaceFileFormComponent', () => { + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + TestHostComponent, + TestUploadComponent, + ReplaceFileFormComponent + ], + imports: [ + MatIconModule, + TranslateModule.forRoot() + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + expect(testHostComponent).toBeTruthy(); + }); + + it('generate the error messages for a still image file representation', () => { + expect(testHostComponent.replaceFileFormComp.warningMessages[0]).toEqual('Image will be replaced.'); + expect(testHostComponent.replaceFileFormComp.warningMessages[1]).toEqual('Please note that you are about to replace the image.'); + }); +}); diff --git a/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.ts b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.ts new file mode 100644 index 0000000000..83904b879c --- /dev/null +++ b/src/app/workspace/resource/representation/replace-file-form/replace-file-form.component.ts @@ -0,0 +1,73 @@ +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { UpdateFileValue } from '@dasch-swiss/dsp-js'; +import { UploadComponent } from '../upload/upload.component'; + +@Component({ + selector: 'app-replace-file-form', + templateUrl: './replace-file-form.component.html', + styleUrls: ['./replace-file-form.component.scss'] +}) +export class ReplaceFileFormComponent implements OnInit { + @Input() representation: 'stillImage' | 'movingImage' | 'audio' | 'document' | 'text' | 'archive'; + @Input() propId: string; + + @Output() closeDialog: EventEmitter = new EventEmitter(); + + @ViewChild('upload') uploadComponent: UploadComponent; + + fileValue: UpdateFileValue; + warningMessages: string[]; + + constructor() { } + + ngOnInit(): void { + this._generateWarningMessage(this.representation); + } + + setFileValue(file: UpdateFileValue) { + this.fileValue = file; + } + + saveFile() { + const updateVal = this.uploadComponent.getUpdatedValue(this.propId); + + if(updateVal instanceof UpdateFileValue) { + updateVal.filename = this.fileValue.filename; + updateVal.id = this.propId; + this.closeDialog.emit(updateVal); + } else { + console.log('expected UpdateFileValue, got: ', updateVal); + } + } + + // generate the warning message strings with the correct representation type + _generateWarningMessage(representationType: string) { + + this.warningMessages = []; + + if(representationType === undefined){ + this.warningMessages.push('File will be replaced.'); + this.warningMessages.push('Please note that you are about to replace the file'); + } + + let repType = representationType; + + if (representationType === 'stillImage' || representationType === 'movingImage') { + switch (representationType) { + case 'stillImage': + repType = 'image'; + break; + + case 'movingImage': + repType = 'video'; + break; + } + } + + const capitalized = repType[0].toUpperCase() + repType.substring(1).toLowerCase(); + + this.warningMessages.push(capitalized + ' will be replaced.'); + this.warningMessages.push('Please note that you are about to replace the ' + repType + '.'); + } + +} diff --git a/src/app/workspace/resource/representation/still-image/still-image.component.html b/src/app/workspace/resource/representation/still-image/still-image.component.html index 3e60c3308e..b5f2ea2268 100644 --- a/src/app/workspace/resource/representation/still-image/still-image.component.html +++ b/src/app/workspace/resource/representation/still-image/still-image.component.html @@ -42,6 +42,9 @@ +
diff --git a/src/app/workspace/resource/representation/still-image/still-image.component.spec.ts b/src/app/workspace/resource/representation/still-image/still-image.component.spec.ts index 691a55edf5..f8e814a21c 100644 --- a/src/app/workspace/resource/representation/still-image/still-image.component.spec.ts +++ b/src/app/workspace/resource/representation/still-image/still-image.component.spec.ts @@ -1,13 +1,19 @@ +import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; +import { OverlayContainer } from '@angular/cdk/overlay'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { HttpClientModule } from '@angular/common/http'; import { Component, OnInit, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatDialogModule } from '@angular/material/dialog'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatDialogHarness } from '@angular/material/dialog/testing'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatToolbarModule } from '@angular/material/toolbar'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Constants, ReadGeomValue, ReadResource, ReadValue } from '@dasch-swiss/dsp-js'; +import { Constants, MockResource, ReadGeomValue, ReadResource, ReadValue } from '@dasch-swiss/dsp-js'; import { AppInitService } from 'src/app/app-init.service'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; import { FileRepresentation } from '../file-representation'; @@ -79,6 +85,7 @@ function makeRegion(geomString: string[], iri: string): ReadResource { [iiifUrl]="iiifUrl" [activateRegion]="inputActivateRegion" [currentTab]="'annotations'" + [parentResource]="readresource" (regionClicked)="regHovered($event)"> ` }) @@ -86,6 +93,7 @@ class TestHostComponent implements OnInit { @ViewChild(StillImageComponent) osdViewerComp: StillImageComponent; + readResource: ReadResource; stillImageFileRepresentations: FileRepresentation[] = []; caption = 'test image'; iiifUrl = 'https://iiif.test.dasch.swiss:443/0803/incunabula_0000003840.jp2/full/3210,5144/0/default.jpg'; @@ -95,6 +103,10 @@ class TestHostComponent implements OnInit { ngOnInit() { + MockResource.getTestThing().subscribe(res => { + this.readResource = res; + }); + this.stillImageFileRepresentations = [ new FileRepresentation(stillImageFileValue, @@ -112,6 +124,8 @@ class TestHostComponent implements OnInit { describe('StillImageComponent', () => { let testHostComponent: TestHostComponent; let testHostFixture: ComponentFixture; + let rootLoader: HarnessLoader; + let overlayContainer: OverlayContainer; beforeEach(waitForAsync(() => { @@ -124,7 +138,8 @@ describe('StillImageComponent', () => { TestBed.configureTestingModule({ declarations: [ StillImageComponent, - TestHostComponent + TestHostComponent, + CdkCopyToClipboard ], imports: [ BrowserAnimationsModule, @@ -140,6 +155,14 @@ describe('StillImageComponent', () => { provide: DspApiConnectionToken, useValue: adminSpyObj }, + { + provide: MAT_DIALOG_DATA, + useValue: {} + }, + { + provide: MatDialogRef, + useValue: {} + }, ] }) .compileComponents(); @@ -149,6 +172,9 @@ describe('StillImageComponent', () => { testHostFixture = TestBed.createComponent(TestHostComponent); testHostComponent = testHostFixture.componentInstance; testHostFixture.detectChanges(); + + overlayContainer = TestBed.inject(OverlayContainer); + rootLoader = TestbedHarnessEnvironment.documentRootLoader(testHostFixture); }); it('should create', () => { @@ -228,4 +254,14 @@ describe('StillImageComponent', () => { }); + // it('should open the dialog box when the replace image button is clicked', async () => { + + // const replaceImageButton = await rootLoader.getHarness(MatButtonHarness.with({ selector: '.replace-image' })); + // console.log('replaceImageButton: ', replaceImageButton); + // await replaceImageButton.click(); + + // const dialogHarnesses = await rootLoader.getAllHarnesses(MatDialogHarness); + // expect(dialogHarnesses.length).toEqual(1); + // }); + }); diff --git a/src/app/workspace/resource/representation/still-image/still-image.component.ts b/src/app/workspace/resource/representation/still-image/still-image.component.ts index e11152b7cf..fccd705a60 100644 --- a/src/app/workspace/resource/representation/still-image/still-image.component.ts +++ b/src/app/workspace/resource/representation/still-image/still-image.component.ts @@ -6,6 +6,7 @@ import { Input, OnChanges, OnDestroy, + OnInit, Output, SimpleChanges } from '@angular/core'; @@ -13,8 +14,10 @@ import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { + ApiResponseError, Constants, CreateColorValue, + CreateFileValue, CreateGeomValue, CreateLinkValue, CreateResource, @@ -26,14 +29,20 @@ import { ReadGeomValue, ReadResource, ReadStillImageFileValue, - RegionGeometry + RegionGeometry, + UpdateFileValue, + UpdateResource, + UpdateValue, + WriteValueResponse } from '@dasch-swiss/dsp-js'; import * as OpenSeadragon from 'openseadragon'; +import { mergeMap } from 'rxjs/operators'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; import { DialogComponent } from 'src/app/main/dialog/dialog.component'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; import { NotificationService } from 'src/app/main/services/notification.service'; import { DspCompoundPosition } from '../../dsp-resource'; +import { EmitEvent, Events, UpdatedFileEventValue, ValueOperationEventService } from '../../services/value-operation-event.service'; import { FileRepresentation } from '../file-representation'; @@ -114,6 +123,7 @@ export class StillImageComponent implements OnChanges, OnDestroy { @Input() activateRegion?: string; // highlight a region @Input() compoundNavigation?: DspCompoundPosition; @Input() currentTab: string; + @Input() parentResource: ReadResource; @Output() goToPage = new EventEmitter(); @@ -132,7 +142,8 @@ export class StillImageComponent implements OnChanges, OnDestroy { private _elementRef: ElementRef, private _errorHandler: ErrorHandlerService, private _matIconRegistry: MatIconRegistry, - private _notification: NotificationService + private _notification: NotificationService, + private _valueOperationEventService: ValueOperationEventService ) { OpenSeadragon.setString('Tooltips.Home', ''); OpenSeadragon.setString('Tooltips.ZoomIn', ''); @@ -334,6 +345,49 @@ export class StillImageComponent implements OnChanges, OnDestroy { this._notification.openSnackBar(message); } + openReplaceFileDialog(){ + const propId = this.parentResource.properties[Constants.HasStillImageFileValue][0].id; + + const dialogConfig: MatDialogConfig = { + width: '800px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { mode: 'replaceFile', title: '2D Image (Still Image)', subtitle: 'Update image of the resource' , representation: 'stillImage', id: propId }, + disableClose: true + }; + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe((data) => { + this._replaceFile(data); + }); + } + + private _replaceFile(file: UpdateFileValue) { + const updateRes = new UpdateResource(); + updateRes.id = this.parentResource.id; + updateRes.type = this.parentResource.type; + updateRes.property = Constants.HasStillImageFileValue; + updateRes.value = file; + + this._dspApiConnection.v2.values.updateValue(updateRes as UpdateResource).pipe( + mergeMap((res: WriteValueResponse) => this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid)) + ).subscribe( + (res2: ReadResource) => { + this._valueOperationEventService.emit( + new EmitEvent(Events.FileValueUpdated, new UpdatedFileEventValue( + res2.properties[Constants.HasStillImageFileValue][0]))); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } + /** * opens the dialog to enter further properties for the region after it has been drawn and calls the function to upload the region after confirmation * @param startPoint the start point of the drawing diff --git a/src/app/workspace/resource/resource.component.html b/src/app/workspace/resource/resource.component.html index b9b7b3da9f..84fb7c9817 100644 --- a/src/app/workspace/resource/resource.component.html +++ b/src/app/workspace/resource/resource.component.html @@ -13,17 +13,18 @@ [resourceIri]="incomingResource ? incomingResource.res.id : resource.res.id" [project]="resource.res.attachedToProject" [currentTab]="selectedTabLabel" + [parentResource]="resource.res" (goToPage)="compoundNavigation($event)" (regionClicked)="openRegion($event)" (regionAdded)="updateRegions($event)"> - + - + - + The file representation type "{{representationsToDisplay[0].fileValue.type}}" is not yet implemented diff --git a/src/app/workspace/resource/resource.component.ts b/src/app/workspace/resource/resource.component.ts index b405ba0076..d7dcdb0c1b 100644 --- a/src/app/workspace/resource/resource.component.ts +++ b/src/app/workspace/resource/resource.component.ts @@ -31,7 +31,7 @@ import { FileRepresentation, RepresentationConstants } from './representation/fi import { Region, StillImageComponent } from './representation/still-image/still-image.component'; import { IncomingService } from './services/incoming.service'; import { ResourceService } from './services/resource.service'; -import { ValueOperationEventService } from './services/value-operation-event.service'; +import { Events, UpdatedFileEventValue, ValueOperationEventService } from './services/value-operation-event.service'; @Component({ selector: 'app-resource', @@ -92,6 +92,8 @@ export class ResourceComponent implements OnInit, OnChanges, OnDestroy { navigationSubscription: Subscription; + valueOperationEventSubscriptions: Subscription[] = []; + constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _errorHandler: ErrorHandlerService, @@ -102,6 +104,7 @@ export class ResourceComponent implements OnInit, OnChanges, OnDestroy { private _router: Router, private _session: SessionService, private _titleService: Title, + private _valueOperationEventService: ValueOperationEventService ) { this._route.params.subscribe(params => { @@ -139,6 +142,13 @@ export class ResourceComponent implements OnInit, OnChanges, OnDestroy { } }); + + this.valueOperationEventSubscriptions.push(this._valueOperationEventService.on( + Events.FileValueUpdated, (newFileValue: UpdatedFileEventValue) => { + if (newFileValue) { + this.getResource(this.resourceIri); + } + })); } ngOnInit() { @@ -164,6 +174,11 @@ export class ResourceComponent implements OnInit, OnChanges, OnDestroy { if (this.navigationSubscription !== undefined) { this.navigationSubscription.unsubscribe(); } + + // unsubscribe from the ValueOperationEventService when component is destroyed + if (this.valueOperationEventSubscriptions !== undefined) { + this.valueOperationEventSubscriptions.forEach(sub => sub.unsubscribe()); + } } @@ -265,7 +280,6 @@ export class ResourceComponent implements OnInit, OnChanges, OnDestroy { // gather system property information res.systemProps = this.resource.res.entityInfo.getPropertyDefinitionsByType(SystemPropertyDefinition); } - this.loading = false; }, (error: ApiResponseError) => { diff --git a/src/app/workspace/resource/services/value-operation-event.service.ts b/src/app/workspace/resource/services/value-operation-event.service.ts index 84da15c77d..8f073d9264 100644 --- a/src/app/workspace/resource/services/value-operation-event.service.ts +++ b/src/app/workspace/resource/services/value-operation-event.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { DeleteValue, ReadValue } from '@dasch-swiss/dsp-js'; +import { DeleteValue, ReadFileValue, ReadValue } from '@dasch-swiss/dsp-js'; import { Subject, Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @@ -46,7 +46,8 @@ export class EmitEvent { export enum Events { ValueAdded, ValueDeleted, - ValueUpdated + ValueUpdated, + FileValueUpdated } export abstract class EventValue { } @@ -68,3 +69,9 @@ export class DeletedEventValue extends EventValue { super(); } } + +export class UpdatedFileEventValue extends EventValue { + constructor(public updatedFileValue: ReadValue) { + super(); + } +}