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(text-file): add support for text file representations (DEV-920) #751

Merged
merged 2 commits into from May 25, 2022
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
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