Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(representation): add support for uploading and viewing archive files (DEV-18) #600

Merged
merged 4 commits into from Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -320,6 +321,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
UsersListComponent,
VisualizerComponent,
YetAnotherDateValueComponent,
ArchiveComponent,
],
imports: [
AngularSplitModule.forRoot(),
Expand Down
Expand Up @@ -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'
]
}
];
}
Expand Down
Expand Up @@ -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
Expand Down
@@ -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>

Empty file.
@@ -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,
Expand All @@ -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[]
) {

Expand All @@ -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;
Expand Down
@@ -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,
Expand All @@ -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;

Expand All @@ -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',
Expand Down Expand Up @@ -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':
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down