Skip to content

Commit

Permalink
feat(text-file): add support for text file representations (DEV-920) (#…
Browse files Browse the repository at this point in the history
…751)

* feat(text-file): add support for text file representations

* fix(upload): add text/csv MIME type
  • Loading branch information
mdelez committed May 25, 2022
1 parent c3109d3 commit 84975d7
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 21 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -34,7 +34,7 @@
"@angular/platform-browser-dynamic": "^13.2.6",
"@angular/router": "^13.2.6",
"@ckeditor/ckeditor5-angular": "^2.0.2",
"@dasch-swiss/dsp-js": "^7.2.1",
"@dasch-swiss/dsp-js": "^7.3.0",
"@datadog/browser-rum": "^3.11.0",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "6.0.0",
Expand Down
4 changes: 3 additions & 1 deletion src/app/app.module.ts
Expand Up @@ -160,6 +160,7 @@ import { ExpertSearchComponent } from './workspace/search/expert-search/expert-s
import { FulltextSearchComponent } from './workspace/search/fulltext-search/fulltext-search.component';
import { SearchPanelComponent } from './workspace/search/search-panel/search-panel.component';
import { HintComponent } from './main/action/hint/hint.component';
import { TextComponent } from './workspace/resource/representation/text/text.component';

// translate: AoT requires an exported function for factories
export function httpLoaderFactory(httpClient: HttpClient) {
Expand Down Expand Up @@ -306,7 +307,8 @@ export function httpLoaderFactory(httpClient: HttpClient) {
UsersListComponent,
VideoComponent,
VideoPreviewComponent,
HintComponent
HintComponent,
TextComponent
],
imports: [
AngularSplitModule.forRoot(),
Expand Down
2 changes: 1 addition & 1 deletion src/app/main/status/status.component.ts
Expand Up @@ -25,7 +25,7 @@ export class StatusComponent implements OnInit {

@Input() comment?: string;
@Input() url?: string;
@Input() representation?: 'archive' | 'audio' | 'document' | 'still-image' | 'video';
@Input() representation?: 'archive' | 'audio' | 'document' | 'still-image' | 'video' | 'text';

refresh = false;

Expand Down
Expand Up @@ -64,7 +64,9 @@ export class CreateLinkResourceComponent implements OnInit {
prop.id !== Constants.HasStillImageFileValue &&
prop.id !== Constants.HasDocumentFileValue &&
prop.id !== Constants.HasAudioFileValue &&
prop.id !== Constants.HasArchiveFileValue
prop.id !== Constants.HasArchiveFileValue &&
prop.id !== Constants.HasMovingImageFileValue &&
prop.id !== Constants.HasTextFileValue
);

if (onto.properties[Constants.HasStillImageFileValue]) {
Expand All @@ -75,6 +77,10 @@ export class CreateLinkResourceComponent implements OnInit {
this.hasFileValue = 'audio';
} else if (onto.properties[Constants.HasArchiveFileValue]) {
this.hasFileValue = 'archive';
} else if (onto.properties[Constants.HasMovingImageFileValue]) {
this.hasFileValue = 'movingImage';
} else if (onto.properties[Constants.HasTextFileValue]) {
this.hasFileValue = 'text';
} else {
this.hasFileValue = undefined;
}
Expand Down
@@ -1,6 +1,6 @@
<div *ngIf="src && src.fileValue.fileUrl">
<!-- in case of an error -->
<app-status [status]="404" [url]="src.fileValue.fileUrl" [representation]="'audio'" *ngIf="failedToLoad"></app-status>
<app-status [status]="404" [url]="src.fileValue.fileUrl" [representation]="'archive'" *ngIf="failedToLoad"></app-status>

<button class="download" mat-button (click)="downloadArchive(src.fileValue.fileUrl)" [disabled]="failedToLoad">
<mat-icon>
Expand Down
17 changes: 17 additions & 0 deletions src/app/workspace/resource/representation/text/text.component.html
@@ -0,0 +1,17 @@
<div *ngIf="src && src.fileValue.fileUrl">
<!-- in case of an error -->
<app-status [status]="404" [url]="src.fileValue.fileUrl" [representation]="'text'" *ngIf="failedToLoad"></app-status>

<button class="download" mat-button (click)="downloadText(src.fileValue.fileUrl)" [disabled]="failedToLoad">
<mat-icon>
file_download
</mat-icon>
Click to download
</button>
<button mat-button matTooltip="Replace text file" (click)="openReplaceFileDialog()">
<mat-icon>cloud_upload</mat-icon>
</button>
</div>
<div *ngIf="!src || !src.fileValue.fileUrl">
No valid file url found for this resource.
</div>
Empty file.
109 changes: 109 additions & 0 deletions src/app/workspace/resource/representation/text/text.component.spec.ts
@@ -0,0 +1,109 @@
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 { KnoraApiConnection } from '@dasch-swiss/dsp-js';
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 { TextComponent } from './text.component';

const textFileValue = {
'arkUrl': 'http://0.0.0.0:3336/ark:/72163/1/9876/=wcU1HzYTEKbJCYPybyKmAs/Kp81r_BPTHKa4oSd5iIxXgd',
'attachedToUser': 'http://rdfh.ch/users/root',
'fileUrl': 'http://0.0.0.0:1024/9876/Jjic1ixccX7-BUHCAFNlEts.txt/file',
'filename': 'Jjic1ixccX7-BUHCAFNlEts.txt',
'hasPermissions': 'CR knora-admin:ProjectAdmin|M knora-admin:ProjectMember',
'id': 'http://rdfh.ch/9876/-wcU1HzYTEKbJCYPybyKmA/values/95Ny4a1_S6ey5JQZWjf07g',
'property': 'http://api.knora.org/ontology/knora-api/v2#hasTextFileValue',
'propertyComment': 'Connects a Representation to a text file',
'propertyLabel': 'hat Textdatei',
'strval': 'http://0.0.0.0:1024/9876/Jjic1ixccX7-BUHCAFNlEts.txt/file',
'type': 'http://api.knora.org/ontology/knora-api/v2#TextFileValue',
'userHasPermission': 'CR',
'uuid': 'Kp81r_BPTHKa4oSd5iIxXg',
'valueCreationDate': '2022-05-25T09:20:19.907631398Z',
'valueHasComment': undefined,
'versionArkUrl': 'http://0.0.0.0:3336/ark:/72163/1/9876/=wcU1HzYTEKbJCYPybyKmAs/Kp81r_BPTHKa4oSd5iIxXgd.20220525T092019907631398Z'
};

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

@ViewChild(TextComponent) textComp: TextComponent;

textFileRepresentation: FileRepresentation;

ngOnInit() {

this.textFileRepresentation = new FileRepresentation(textFileValue);
}
}
describe('TextComponent', () => {
let testHostComponent: TestHostComponent;
let testHostFixture: ComponentFixture<TestHostComponent>;
let loader: HarnessLoader;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
TextComponent,
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.textFileRepresentation.fileValue.fileUrl).toEqual('http://0.0.0.0:1024/9876/Jjic1ixccX7-BUHCAFNlEts.txt/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.textFileRepresentation = undefined;
testHostFixture.detectChanges();

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

expect(downloadButtonElement.length).toEqual(0);
});
});
140 changes: 140 additions & 0 deletions src/app/workspace/resource/representation/text/text.component.ts
@@ -0,0 +1,140 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { AfterViewInit, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { ApiResponseError, Constants, KnoraApiConnection, ReadTextFileValue, ReadResource, UpdateFileValue, UpdateResource, UpdateValue, WriteValueResponse } 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/services/error-handler.service';
import { EmitEvent, Events, UpdatedFileEventValue, ValueOperationEventService } from '../../services/value-operation-event.service';
import { FileRepresentation } from '../file-representation';
import { RepresentationService } from '../representation.service';

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

@Input() src: FileRepresentation;

@Input() parentResource: ReadResource;

@Output() loaded = new EventEmitter<boolean>();

originalFilename: string;

failedToLoad = false;

constructor(
@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection,
private readonly _http: HttpClient,
private _dialog: MatDialog,
private _errorHandler: ErrorHandlerService,
private _rs: RepresentationService,
private _valueOperationEventService: ValueOperationEventService
) { }

ngOnInit(): void {
this._getOriginalFilename();
this.failedToLoad = !this._rs.doesFileExist(this.src.fileValue.fileUrl);
}

ngAfterViewInit() {
this.loaded.emit(true);
}

// https://stackoverflow.com/questions/66986983/angular-10-download-file-from-firebase-link-without-opening-into-new-tab
async downloadText(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;

// set filename
if (this.originalFilename === undefined) {
e.download = url.substr(url.lastIndexOf('/') + 1);
} else {
e.download = this.originalFilename;
}

document.body.appendChild(e);
e.click();
document.body.removeChild(e);
}

openReplaceFileDialog(){
const propId = this.parentResource.properties[Constants.HasTextFileValue][0].id;

const dialogConfig: MatDialogConfig = {
width: '800px',
maxHeight: '80vh',
position: {
top: '112px'
},
data: { mode: 'replaceFile', title: 'Text (csv, txt, xml)', subtitle: 'Update the text file of this resource' , representation: 'text', id: propId },
disableClose: true
};
const dialogRef = this._dialog.open(
DialogComponent,
dialogConfig
);

dialogRef.afterClosed().subscribe((data) => {
if (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.HasTextFileValue;
updateRes.value = file;

this._dspApiConnection.v2.values.updateValue(updateRes as UpdateResource<UpdateValue>).pipe(
mergeMap((res: WriteValueResponse) => this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid))
).subscribe(
(res2: ReadResource) => {
this.src.fileValue.fileUrl = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).fileUrl;
this.src.fileValue.filename = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).filename;
this.src.fileValue.strval = (res2.properties[Constants.HasTextFileValue][0] as ReadTextFileValue).strval;

this._getOriginalFilename();

this._valueOperationEventService.emit(
new EmitEvent(Events.FileValueUpdated, new UpdatedFileEventValue(
res2.properties[Constants.HasTextFileValue][0])));
},
(error: ApiResponseError) => {
this._errorHandler.showMessage(error);
}
);
}
}
Expand Up @@ -9,10 +9,13 @@
<app-progress-indicator *ngIf="isLoading"></app-progress-indicator>
<p class="title">Upload file<br>
<span class="mat-body-1">The following file types are supported:<br>
<span *ngFor="let item of allowedFileTypes; let last = last">{{ item | split: '/':1 }}
<span *ngIf="!last">,&nbsp;</span>
<span *ngIf="representation !== 'audio' && representation !== 'text'">
<span *ngFor="let item of allowedFileTypes; let last = last">{{ item | split: '/':1 }}
<span *ngIf="!last">,&nbsp;</span>
</span>
</span>
<span *ngIf="representation === 'audio'">&nbsp;(= mp3)</span>
<span *ngIf="representation === 'audio'">&nbsp;mp3</span>
<span *ngIf="representation === 'text'">&nbsp;csv, txt, xml</span>
</span>
</p>
<div class="bottom-line">
Expand Down

0 comments on commit 84975d7

Please sign in to comment.