Skip to content
Permalink
Browse files
feat(representation): add support for uploading and viewing archive f…
…iles (DEV-18) (#600)

* feat(archive): adds support for uploading and viewing archive files

* test(archive): adds test to archive comp

* test(archive): fixes import
  • Loading branch information
mdelez committed Dec 6, 2021
1 parent 0980717 commit 9bb63d71234fc49f2afcb959603b8bcd4deb4429
Show file tree
Hide file tree
Showing 17 changed files with 248 additions and 23 deletions.

Some generated files are not rendered by default. Learn more.

@@ -18,7 +18,7 @@
"test-local": "ng test",
"test-e2e-protractor": "ng e2e --configuration production --protractor-config=./e2e/protractor-ci.conf.js --webdriver-update=false",
"webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.37",
"yalc-add-lib": "rm -rf .yalc/@dasch-swiss && yalc add @dasch-swiss/dsp-js && yalc add @dasch-swiss/dsp-ui && npm install"
"yalc-add-lib": "rm -rf .yalc/@dasch-swiss && yalc add @dasch-swiss/dsp-js && npm install"
},
"private": true,
"dependencies": {
@@ -33,7 +33,7 @@
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@ckeditor/ckeditor5-angular": "^2.0.2",
"@dasch-swiss/dsp-js": "^5.0.0",
"@dasch-swiss/dsp-js": "^5.1.0",
"@datadog/browser-rum": "^3.7.0",
"@ngx-translate/core": "^12.1.2",
"@ngx-translate/http-loader": "5.0.0",
@@ -166,6 +166,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 { ArchiveComponent } from './workspace/resource/representation/archive/archive.component';

// translate: AoT requires an exported function for factories
export function httpLoaderFactory(httpClient: HttpClient) {
@@ -320,6 +321,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
UsersListComponent,
VisualizerComponent,
YetAnotherDateValueComponent,
ArchiveComponent,
],
imports: [
AngularSplitModule.forRoot(),
@@ -140,6 +140,15 @@ export class DefaultResourceClasses {
'drafts',
'library_books'
]
},
{
iri: Constants.KnoraApiV2 + Constants.HashDelimiter + 'ArchiveRepresentation',
label: 'Archive (zip, x-tar, gzip)',
icons: [
'archive',
'folder',
'folder_open'
]
}
];
}
@@ -101,6 +101,7 @@ <h3 class="label mat-title">
prop.propDef.objectType !== representationConstants.audio &&
prop.propDef.objectType !== representationConstants.document &&
prop.propDef.objectType !== representationConstants.text &&
prop.propDef.objectType !== representationConstants.archive &&
!(isAnnotation && (
prop.propDef.subjectType === representationConstants.region &&
prop.propDef.objectType !== representationConstants.color
@@ -0,0 +1,12 @@
<div *ngIf="src && src.fileValue.fileUrl">
<button class="download" mat-button (click)="downloadArchive(src.fileValue.fileUrl)">
<mat-icon>
file_download
</mat-icon>
Click to download
</button>
</div>
<div *ngIf="!src || !src.fileValue.fileUrl">
No valid file url found for this resource.
</div>

@@ -0,0 +1,110 @@
import { KnoraApiConnection } from '@dasch-swiss/dsp-js';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { AppInitService } from 'src/app/app-init.service';
import { DspApiConfigToken, DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens';
import { TestConfig } from 'test.config';
import { FileRepresentation } from '../file-representation';

import { ArchiveComponent } from './archive.component';

const archiveFileValue = {
'arkUrl': 'http://0.0.0.0:3336/ark:/72163/1/0123/6c=f69h6Ss6GXPME565EqAS/dDHcFHlwQ9K46255QfUGrQ8',
'attachedToUser': 'http://rdfh.ch/users/root',
'fileUrl': 'http://0.0.0.0:1024/0123/Eu71soNXOAL-DVweVgODkFh.zip/file',
'filename': 'Eu71soNXOAL-DVweVgODkFh.zip',
'hasPermissions': 'CR knora-admin:ProjectAdmin|D knora-admin:ProjectAdmin|M knora-admin:ProjectAdmin|V knora-admin:ProjectAdmin|RV knora-admin:ProjectAdmin',
'id': 'http://rdfh.ch/0123/6c-f69h6Ss6GXPME565EqA/values/dDHcFHlwQ9K46255QfUGrQ',
'property': 'http://api.knora.org/ontology/knora-api/v2#hasArchiveFileValue',
'propertyComment': 'Connects a Representation to a zip archive',
'propertyLabel': 'hat Zip',
'strval': 'http://0.0.0.0:1024/0123/Eu71soNXOAL-DVweVgODkFh.zip/file',
'type': 'http://api.knora.org/ontology/knora-api/v2#ArchiveFileValue',
'userHasPermission': 'CR',
'uuid': 'dDHcFHlwQ9K46255QfUGrQ',
'valueCreationDate': '2021-12-03T09:59:46.609839Z',
'valueHasComment': undefined,
'versionArkUrl': 'http://0.0.0.0:3336/ark:/72163/1/0123/6c=f69h6Ss6GXPME565EqAS/dDHcFHlwQ9K46255QfUGrQ8.20211203T095946609839Z'
};

@Component({
template: `
<app-archive [src]="archiveFileRepresentation">
</app-archive>`
})
class TestHostComponent implements OnInit {

@ViewChild(ArchiveComponent) archiveComp: ArchiveComponent;

archiveFileRepresentation: FileRepresentation;

ngOnInit() {

this.archiveFileRepresentation = new FileRepresentation(archiveFileValue);
}
}

describe('ArchiveComponent', () => {
let testHostComponent: TestHostComponent;
let testHostFixture: ComponentFixture<TestHostComponent>;
let loader: HarnessLoader;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ArchiveComponent,
TestHostComponent
],
imports: [
HttpClientTestingModule,
MatDialogModule,
MatSnackBarModule
],
providers: [
AppInitService,
{
provide: DspApiConfigToken,
useValue: TestConfig.ApiConfig
},
{
provide: DspApiConnectionToken,
useValue: new KnoraApiConnection(TestConfig.ApiConfig)
}
]
})
.compileComponents();
});

beforeEach(() => {
testHostFixture = TestBed.createComponent(TestHostComponent);
testHostComponent = testHostFixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(testHostFixture);
testHostFixture.detectChanges();
expect(testHostComponent).toBeTruthy();
});

it('should have a file url', () => {
expect(testHostComponent.archiveFileRepresentation.fileValue.fileUrl).toEqual('http://0.0.0.0:1024/0123/Eu71soNXOAL-DVweVgODkFh.zip/file');
});

it('should show a download button if the file url is provided', async () => {
const downloadButtonElement = await loader.getHarness(MatButtonHarness.with({ selector: '.download' }));

expect(downloadButtonElement).toBeTruthy();
});

it('should NOT show a download button if the file url is NOT provided', async () => {
testHostComponent.archiveFileRepresentation = undefined;
testHostFixture.detectChanges();

const downloadButtonElement = await loader.getAllHarnesses(MatButtonHarness.with({ selector: '.download' }));

expect(downloadButtonElement.length).toEqual(0);
});
});
@@ -0,0 +1,41 @@
import { HttpClient } from '@angular/common/http';
import { Component, Input, OnInit } from '@angular/core';
import { ErrorHandlerService } from 'src/app/main/error/error-handler.service';
import { FileRepresentation } from '../file-representation';

@Component({
selector: 'app-archive',
templateUrl: './archive.component.html',
styleUrls: ['./archive.component.scss']
})
export class ArchiveComponent implements OnInit {

@Input() src: FileRepresentation;

constructor(
private readonly _http: HttpClient,
private _errorHandler: ErrorHandlerService
) { }

ngOnInit(): void { }

// https://stackoverflow.com/questions/66986983/angular-10-download-file-from-firebase-link-without-opening-into-new-tab
async downloadArchive(url: string) {
try {
const res = await this._http.get(url, { responseType: 'blob' }).toPromise();
this.downloadFile(res);
} catch (e) {
this._errorHandler.showMessage(e);
}
}

downloadFile(data) {
const url = window.URL.createObjectURL(data);
const e = document.createElement('a');
e.href = url;
e.download = url.substr(url.lastIndexOf('/') + 1);
document.body.appendChild(e);
e.click();
document.body.removeChild(e);
}
}
@@ -1,5 +1,6 @@
import {
Constants,
ReadArchiveFileValue,
ReadAudioFileValue,
ReadDocumentFileValue,
ReadMovingImageFileValue,
@@ -14,11 +15,11 @@ export class FileRepresentation {

/**
*
* @param fileValue a [[ReadAudioFileValue | ReadDocumentFileValue | ReadMovingImageFileValue | ReadStillImageFileValue]] representing a file value
* @param fileValue a [[ReadAudioFileValue | ReadDocumentFileValue | ReadMovingImageFileValue | ReadStillImageFileValue | ReadArchiveFileValue]] representing a file value
* @param annotations[] an array of [[Region]] --> TODO: will be expanded with [[Sequence]]
*/
constructor(
readonly fileValue: ReadAudioFileValue | ReadDocumentFileValue | ReadMovingImageFileValue | ReadStillImageFileValue,
readonly fileValue: ReadAudioFileValue | ReadDocumentFileValue | ReadMovingImageFileValue | ReadStillImageFileValue | ReadArchiveFileValue,
readonly annotations?: Region[]
) {

@@ -31,6 +32,7 @@ export class RepresentationConstants {
static document = Constants.DocumentFileValue;
static movingImage = Constants.MovingImageFileValue;
static stillImage = Constants.StillImageFileValue;
static archive = Constants.ArchiveFileValue;
static text = Constants.TextFileValue;
static region = Constants.Region;
static color = Constants.ColorValue;
@@ -1,10 +1,12 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import {
CreateArchiveFileValue,
CreateAudioFileValue,
CreateDocumentFileValue,
CreateFileValue,
CreateStillImageFileValue,
UpdateArchiveFileValue,
UpdateAudioFileValue,
UpdateDocumentFileValue,
UpdateFileValue,
@@ -26,8 +28,7 @@ export class UploadComponent implements OnInit {

@Input() parentForm?: FormGroup;

@Input() representation: 'stillImage' | 'movingImage' | 'audio' | 'document' | 'text';
// only StillImageRepresentation and DocumentPresentation is supported so far
@Input() representation: 'stillImage' | 'movingImage' | 'audio' | 'document' | 'text' | 'archive';

@Input() formName: string;

@@ -44,6 +45,7 @@ export class UploadComponent implements OnInit {
supportedImageTypes = ['image/jpeg', 'image/jp2', 'image/tiff', 'image/tiff-fx', 'image/png'];
supportedDocumentTypes = ['application/pdf'];
supportedAudioTypes = ['audio/mpeg'];
supportedArchiveTypes = ['application/zip', 'application/x-tar', 'application/gzip'];

// readonly fromLabels = {
// upload: 'Upload file',
@@ -209,7 +211,7 @@ export class UploadComponent implements OnInit {

const filename = this.fileControl.value.internalFilename;

let fileValue: CreateStillImageFileValue | CreateDocumentFileValue;
let fileValue: CreateStillImageFileValue | CreateDocumentFileValue | CreateAudioFileValue | CreateArchiveFileValue;

switch (this.representation) {
case 'stillImage':
@@ -224,6 +226,10 @@ export class UploadComponent implements OnInit {
fileValue = new CreateAudioFileValue();
break;

case 'archive':
fileValue = new CreateArchiveFileValue();
break;

default:
// --> TODO for UPLOAD: expand with other representation file types
break;
@@ -248,7 +254,7 @@ export class UploadComponent implements OnInit {

const filename = this.fileControl.value.internalFilename;

let fileValue: UpdateStillImageFileValue | UpdateDocumentFileValue | UpdateAudioFileValue;
let fileValue: UpdateStillImageFileValue | UpdateDocumentFileValue | UpdateAudioFileValue | UpdateArchiveFileValue;


switch (this.representation) {
@@ -264,6 +270,10 @@ export class UploadComponent implements OnInit {
fileValue = new UpdateAudioFileValue();
break;

case 'archive':
fileValue = new UpdateArchiveFileValue();
break;

default:
// --> TODO for UPLOAD: expand with other representation file types
break;
@@ -303,6 +313,10 @@ export class UploadComponent implements OnInit {
this.allowedFileTypes = this.supportedAudioTypes;
break;

case 'archive':
this.allowedFileTypes = this.supportedArchiveTypes;
break;

default:
this.allowedFileTypes = [];
break;

0 comments on commit 9bb63d7

Please sign in to comment.