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(upload)!: add upload form for still images (DSP-1761) #472

Merged
merged 10 commits into from Jun 28, 2021
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Expand Up @@ -91,6 +91,7 @@ import { UserMenuComponent } from './user/user-menu/user-menu.component';
import { UsersComponent } from './system/users/users.component';
import { UsersListComponent } from './system/users/users-list/users-list.component';
import { VisualizerComponent } from './project/ontology/ontology-visualizer/visualizer/visualizer.component';
import { UploadComponent } from './workspace/resource/representation/upload/upload.component';

// translate: AoT requires an exported function for factories
export function httpLoaderFactory(httpClient: HttpClient) {
Expand Down Expand Up @@ -169,6 +170,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
UsersComponent,
UsersListComponent,
VisualizerComponent,
UploadComponent,
],
imports: [
AppRoutingModule,
Expand Down
@@ -0,0 +1,99 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { AppInitService, Session, SessionService } from '@dasch-swiss/dsp-ui';
import { UploadedFileResponse, UploadFileService } from './upload-file.service';

describe('UploadFileService', () => {
let service: UploadFileService;
let httpTestingController: HttpTestingController;

const file = new File(['1'], 'testfile');
const mockUploadData = new FormData();
mockUploadData.append('test', file);

beforeEach(() => {

const appInitSpy = {
config: {
iiifUrl: 'https://sipi.dasch.swiss/'
}
};

const sessionSpy = jasmine.createSpyObj('SessionService', ['getSession']);

TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
{ provide: AppInitService, useValue: appInitSpy },
{ provide: SessionService, useValue: sessionSpy },
]
});

service = TestBed.inject(UploadFileService);
httpTestingController = TestBed.inject(HttpTestingController);

const sessionServiceSpy = TestBed.inject(SessionService) as jasmine.SpyObj<SessionService>;

sessionServiceSpy.getSession.and.callFake(
() => {
const session: Session = {
id: 12345,
user: {
name: 'username',
jwt: 'myToken',
lang: 'en',
sysAdmin: false,
projectAdmin: []
}
};

return session;
}
);
});

afterEach(() => {
// after every test, assert that there are no more pending requests.
httpTestingController.verify();
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should return expected file response on upload', done => {

const expectedResponse: UploadedFileResponse = {
uploadedFiles: [{
fileType: 'image',
internalFilename: '8R0cJE3TSgB-BssuQyeW1rE.jp2',
originalFilename: 'Screenshot 2020-10-28 at 14.16.34.png',
temporaryUrl: 'http://sipi:1024/tmp/8R0cJE3TSgB-BssuQyeW1rE.jp2'
}]
};

service.upload(mockUploadData).subscribe(
res => {
expect(res.uploadedFiles.length).toEqual(1);
expect(res.uploadedFiles[0].internalFilename).toEqual('8R0cJE3TSgB-BssuQyeW1rE.jp2');
done();
}
);

const httpRequest = httpTestingController.expectOne('https://sipi.dasch.swiss/upload?token=myToken');

expect(httpRequest.request.method).toEqual('POST');

const expectedFormData = new FormData();
const mockFile = new File(['1'], 'testfile', { type: 'image/jpeg' });

expectedFormData.append(mockFile.name, mockFile);
expect(httpRequest.request.body).toEqual(expectedFormData);

httpRequest.flush(expectedResponse);

});

});
@@ -0,0 +1,45 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { AppInitService, SessionService } from '@dasch-swiss/dsp-ui';
import { Observable } from 'rxjs';

export interface UploadedFile {
fileType: string;
internalFilename: string;
originalFilename: string;
temporaryUrl: string;
}

export interface UploadedFileResponse {
uploadedFiles: UploadedFile[];
}

@Injectable({
providedIn: 'root'
})
export class UploadFileService {

iiifHost: string = (this._init.config['iiifUrl'].substr(-1) === '/') ? this._init.config['iiifUrl'] : this._init.config['iiifUrl'] + '/';

constructor(
private readonly _init: AppInitService,
private readonly _http: HttpClient,
private readonly _session: SessionService
) { }

/**
* uploads files to SIPI
* @param (file)
*/
upload(file: FormData): Observable<UploadedFileResponse> {
const baseUrl = `${this.iiifHost}upload`;

// checks if user is logged in
const jwt = this._session.getSession()?.user.jwt;
const params = new HttpParams().set('token', jwt);

// --> TODO in order to track the progress change below to true and 'events'
const options = { params, reportProgress: false, observe: 'body' as 'body' };
return this._http.post<UploadedFileResponse>(baseUrl, file, options);
}
}
@@ -0,0 +1,64 @@
<div class="form-container">
<form [formGroup]="form">
<div dspDragDrop
*ngIf="isLoading || !file"
(click)="fileInput.click()"
(fileDropped)="addFile($event)"
class="dd-container">
<input hidden
type="file"
(change)="addFile($event)"
#fileInput />
<mat-icon *ngIf="!isLoading"
class="upload-icon">
cloud_upload
</mat-icon>
<dsp-progress-indicator *ngIf="isLoading"></dsp-progress-indicator>
<div class="title">Upload file</div>
<div class="bottom-line">
Drag and drop or click to upload
</div>
</div>

<ng-container *ngIf="!isLoading && file">
<div class="thumbnail">
<img src="{{ thumbnailUrl }}"
alt="thumbnail" />
<button mat-button
class="delete-file"
title="delete file"
(click)="deleteAttachment()">
<mat-icon>close</mat-icon>
</button>
</div>

<div class="files-list">
<table>
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Last Modified Date</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ file.name }}</td>
<td>{{ convertBytes(file.size) }}</td>
<td>{{ convertDate(file.lastModified) }}</td>
<td>
<button mat-icon-button
class="delete-file"
title="delete file"
(click)="deleteAttachment()">
<mat-icon>delete</mat-icon>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</ng-container>
</form>
</div>
@@ -0,0 +1,86 @@
// overall styles

.full-width {
width: 100%;
}
.delete-file {
&:hover {
color: red;
}
}

// drag & drop input
.dd-container {
height: 200px;
margin: 20px 0;
border: 2px solid #000;
border-radius: 2px;
position: relative;
text-align: center;

&:hover {
cursor: pointer;
background-color: #ddd !important;
}
}
.upload-icon {
width: auto;
font-size: 48px;
padding: 0;
position: inherit;
top: 35%;
}
.title {
bottom: 20%;
}
.bottom-line,
.title {
width: 100%;
position: absolute;
padding: 5px 0;
}
.bottom-line {
bottom: 0;
color: #fff;
background-color: #000;
}

// thumbnail
.thumbnail {
text-align: center;

& button {
position: absolute;
padding: 0;
}

& mat-icon {
position: absolute;
}
}

// files table
.files-list {
display: flex;
justify-content: space-between;
// width: calc(80% + 4px);
margin: 10px auto;
background: #fff;
color: #000;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background-color: #9ecbec;
text-align: left;
}
td,
th {
border: 1px solid #000;
padding: 3px;
}
tr td:last-child {
text-align: center;
}