mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 23:13:04 +00:00
96252: Extract upload-specific code from SharedModule
This commit is contained in:
9
src/app/shared/upload/uploader/uploader-error.model.ts
Normal file
9
src/app/shared/upload/uploader/uploader-error.model.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* An interface that represents the upload error values
|
||||
*/
|
||||
export interface UploaderError {
|
||||
item?: any;
|
||||
response?: any;
|
||||
status?: any;
|
||||
headers?: any;
|
||||
}
|
29
src/app/shared/upload/uploader/uploader-options.model.ts
Normal file
29
src/app/shared/upload/uploader/uploader-options.model.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { RestRequestMethod } from '../../../core/data/rest-request-method';
|
||||
|
||||
export class UploaderOptions {
|
||||
/**
|
||||
* URL of the REST endpoint for file upload.
|
||||
*/
|
||||
url: string;
|
||||
|
||||
authToken: string;
|
||||
|
||||
disableMultipart = false;
|
||||
|
||||
itemAlias: string = null;
|
||||
|
||||
/**
|
||||
* Automatically send out an upload request when adding files
|
||||
*/
|
||||
autoUpload = true;
|
||||
|
||||
/**
|
||||
* Set the max number of files that can be loaded
|
||||
*/
|
||||
maxFileNumber: number;
|
||||
|
||||
/**
|
||||
* The request method to use for the file upload request
|
||||
*/
|
||||
method: RestRequestMethod = RestRequestMethod.POST;
|
||||
}
|
21
src/app/shared/upload/uploader/uploader-properties.model.ts
Normal file
21
src/app/shared/upload/uploader/uploader-properties.model.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { MetadataMap } from '../../../core/shared/metadata.models';
|
||||
|
||||
/**
|
||||
* Properties to send to the REST API for uploading a bitstream
|
||||
*/
|
||||
export class UploaderProperties {
|
||||
/**
|
||||
* A custom name for the bitstream
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Metadata for the bitstream (e.g. dc.description)
|
||||
*/
|
||||
metadata: MetadataMap;
|
||||
|
||||
/**
|
||||
* The name of the bundle to upload the bitstream to
|
||||
*/
|
||||
bundleName: string;
|
||||
}
|
55
src/app/shared/upload/uploader/uploader.component.html
Normal file
55
src/app/shared/upload/uploader/uploader.component.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<div ng2FileDrop
|
||||
*ngIf="(isOverDocumentDropZone | async)"
|
||||
class="ds-document-drop-zone position-fixed h-100 w-100"
|
||||
[class.ds-document-drop-zone-active]="(isOverDocumentDropZone | async)"
|
||||
[uploader]="uploader"
|
||||
(fileOver)="fileOverDocument($event)">
|
||||
</div>
|
||||
<div *ngIf="(isOverDocumentDropZone | async)"
|
||||
class="ds-document-drop-zone-inner position-fixed h-100 w-100 p-2">
|
||||
<div class="ds-document-drop-zone-inner-content position-relative d-flex flex-column justify-content-center text-center h-100 w-100">
|
||||
<p class="text-primary">{{dropOverDocumentMsg | translate}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div [attr.id]="uploaderId" class="row">
|
||||
<div class="col-md-12">
|
||||
<div ng2FileDrop
|
||||
[ngClass]="{'ds-base-drop-zone-file-over': (isOverBaseDropZone | async)}"
|
||||
[uploader]="uploader"
|
||||
(fileOver)="fileOverBase($event)"
|
||||
class="well ds-base-drop-zone mt-1 mb-3 text-muted">
|
||||
<div class="text-center m-0 p-2 d-flex justify-content-center align-items-center" *ngIf="uploader?.queue?.length === 0">
|
||||
<span>
|
||||
<i class="fas fa-upload" aria-hidden="true"></i>
|
||||
{{dropMsg | translate}}{{'uploader.or' | translate}}
|
||||
<label for="inputFileUploader" class="btn btn-link m-0 p-0 ml-1" tabindex="0" (keyup.enter)="$event.stopImmediatePropagation(); fileInput.click()">
|
||||
<span role="button" [attr.aria-label]="'uploader.browse' | translate">{{'uploader.browse' | translate}}</span>
|
||||
</label>
|
||||
<input #fileInput id="inputFileUploader" class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple tabindex="0" />
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="(isOverBaseDropZone | async) || uploader?.queue?.length !== 0">
|
||||
<div class="m-1">
|
||||
<div class="upload-item-top">
|
||||
<span class="filename">
|
||||
<span *ngIf="!uploader.options.disableMultipart">{{'uploader.queue-length' | translate}}: {{ uploader?.queue?.length }} | </span>{{ uploader?.queue[0]?.file.name }}
|
||||
</span>
|
||||
<div class="btn-group btn-group-sm float-right" role="group">
|
||||
<button type="button" class="btn btn-danger" title="{{'uploader.delete.btn-title' | translate}}" (click)="uploader.clearQueue()" [disabled]="!uploader.queue.length">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span *ngIf="uploader.progress < 100 && !(uploader.progress === 0 && !uploader.options.autoUpload)" class="float-right mr-3">{{ uploader.progress }}%</span>
|
||||
<span *ngIf="uploader.progress === 100" class="float-right mr-3">{{'uploader.processing' | translate}}</span>
|
||||
</div>
|
||||
<div class="ds-base-drop-zone-progress clearfix mt-2">
|
||||
<div role="progressbar"
|
||||
style="height: 5px; width: 0;"
|
||||
[ngStyle]="{ 'width': uploader.progress + '%' }"
|
||||
[ngClass]="{'progress-bar': true, 'bg-success progress-bar-striped progress-bar-animated': uploader.progress === 100}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
38
src/app/shared/upload/uploader/uploader.component.scss
Normal file
38
src/app/shared/upload/uploader/uploader.component.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.ds-base-drop-zone {
|
||||
border: 2px dashed var(--bs-gray-600);
|
||||
}
|
||||
|
||||
/* Default class applied to drop zones on over */
|
||||
.ds-base-drop-zone-file-over {
|
||||
border: 2px dashed var(--bs-primary);
|
||||
}
|
||||
|
||||
.ds-base-drop-zone p {
|
||||
min-height: var(--ds-drop-zone-area-height);
|
||||
}
|
||||
|
||||
.ds-document-drop-zone {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-active {
|
||||
z-index: var(--ds-drop-zone-area-z-index) !important;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-inner {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
z-index: var(--ds-drop-zone-area-inner-z-index);
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-inner-content {
|
||||
border: 4px dashed var(--bs-primary);
|
||||
z-index: var(--ds-drop-zone-area-inner-z-index);
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-inner-content p {
|
||||
font-size: calc(var(--bs-font-size-lg) * 2.5);
|
||||
}
|
88
src/app/shared/upload/uploader/uploader.component.spec.ts
Normal file
88
src/app/shared/upload/uploader/uploader.component.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing';
|
||||
|
||||
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
|
||||
|
||||
import { DragService } from '../../../core/drag.service';
|
||||
import { UploaderOptions } from './uploader-options.model';
|
||||
import { UploaderComponent } from './uploader.component';
|
||||
import { FileUploadModule } from 'ng2-file-upload';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { createTestComponent } from '../../testing/utils.test';
|
||||
import { HttpXsrfTokenExtractor } from '@angular/common/http';
|
||||
import { CookieService } from '../../../core/services/cookie.service';
|
||||
import { CookieServiceMock } from '../../mocks/cookie.service.mock';
|
||||
import { HttpXsrfTokenExtractorMock } from '../../mocks/http-xsrf-token-extractor.mock';
|
||||
|
||||
describe('Chips component', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let html;
|
||||
|
||||
// waitForAsync beforeEach
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FileUploadModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
UploaderComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
ScrollToService,
|
||||
UploaderComponent,
|
||||
DragService,
|
||||
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
|
||||
{ provide: CookieService, useValue: new CookieServiceMock() },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-uploader [onBeforeUpload]="onBeforeUpload"
|
||||
[uploadFilesOptions]="uploadFilesOptions"
|
||||
(onCompleteItem)="onCompleteItem($event)"></ds-uploader>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create Uploader Component', inject([UploaderComponent], (app: UploaderComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
public uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), {
|
||||
url: 'http://test',
|
||||
authToken: null,
|
||||
disableMultipart: false,
|
||||
itemAlias: null
|
||||
});
|
||||
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
public onBeforeUpload = () => {
|
||||
};
|
||||
|
||||
onCompleteItem(event) {
|
||||
}
|
||||
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
}
|
228
src/app/shared/upload/uploader/uploader.component.ts
Normal file
228
src/app/shared/upload/uploader/uploader.component.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, Output, ViewEncapsulation, } from '@angular/core';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { FileUploader } from 'ng2-file-upload';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
|
||||
|
||||
import { UploaderOptions } from './uploader-options.model';
|
||||
import { hasValue, isNotEmpty, isUndefined } from '../../empty.util';
|
||||
import { UploaderProperties } from './uploader-properties.model';
|
||||
import { HttpXsrfTokenExtractor } from '@angular/common/http';
|
||||
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.interceptor';
|
||||
import { CookieService } from '../../../core/services/cookie.service';
|
||||
import { DragService } from '../../../core/drag.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-uploader',
|
||||
templateUrl: 'uploader.component.html',
|
||||
styleUrls: ['uploader.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
encapsulation: ViewEncapsulation.Emulated
|
||||
})
|
||||
|
||||
export class UploaderComponent {
|
||||
|
||||
/**
|
||||
* The message to show when drag files on the drop zone
|
||||
*/
|
||||
@Input() dropMsg: string;
|
||||
|
||||
/**
|
||||
* The message to show when drag files on the window document
|
||||
*/
|
||||
@Input() dropOverDocumentMsg: string;
|
||||
|
||||
/**
|
||||
* The message to show when drag files on the window document
|
||||
*/
|
||||
@Input() enableDragOverDocument: boolean;
|
||||
|
||||
/**
|
||||
* The function to call before an upload
|
||||
*/
|
||||
@Input() onBeforeUpload: () => void;
|
||||
|
||||
/**
|
||||
* Configuration for the ng2-file-upload component.
|
||||
*/
|
||||
@Input() uploadFilesOptions: UploaderOptions;
|
||||
|
||||
/**
|
||||
* Extra properties to be passed with the form-data of the upload
|
||||
*/
|
||||
@Input() uploadProperties: UploaderProperties;
|
||||
|
||||
/**
|
||||
* The function to call when upload is completed
|
||||
*/
|
||||
@Output() onCompleteItem: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The function to call on error occurred
|
||||
*/
|
||||
@Output() onUploadError: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The function to call when a file is selected
|
||||
*/
|
||||
@Output() onFileSelected: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
public uploader: FileUploader;
|
||||
public uploaderId: string;
|
||||
public isOverBaseDropZone = observableOf(false);
|
||||
public isOverDocumentDropZone = observableOf(false);
|
||||
|
||||
@HostListener('window:dragover', ['$event'])
|
||||
onDragOver(event: any) {
|
||||
|
||||
if (this.enableDragOverDocument && this.dragService.isAllowedDragOverPage()) {
|
||||
// Show drop area on the page
|
||||
event.preventDefault();
|
||||
if ((event.target as any).tagName !== 'HTML') {
|
||||
this.isOverDocumentDropZone = observableOf(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private cdr: ChangeDetectorRef,
|
||||
private scrollToService: ScrollToService,
|
||||
private dragService: DragService,
|
||||
private tokenExtractor: HttpXsrfTokenExtractor,
|
||||
private cookieService: CookieService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method provided by Angular. Invoked after the constructor.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId();
|
||||
this.checkConfig(this.uploadFilesOptions);
|
||||
this.uploader = new FileUploader({
|
||||
url: this.uploadFilesOptions.url,
|
||||
authToken: this.uploadFilesOptions.authToken,
|
||||
disableMultipart: this.uploadFilesOptions.disableMultipart,
|
||||
itemAlias: this.uploadFilesOptions.itemAlias,
|
||||
removeAfterUpload: true,
|
||||
autoUpload: this.uploadFilesOptions.autoUpload,
|
||||
method: this.uploadFilesOptions.method,
|
||||
queueLimit: this.uploadFilesOptions.maxFileNumber,
|
||||
});
|
||||
|
||||
if (isUndefined(this.enableDragOverDocument)) {
|
||||
this.enableDragOverDocument = false;
|
||||
}
|
||||
if (isUndefined(this.dropMsg)) {
|
||||
this.dropMsg = 'uploader.drag-message';
|
||||
}
|
||||
if (isUndefined(this.dropOverDocumentMsg)) {
|
||||
this.dropOverDocumentMsg = 'uploader.drag-message';
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.uploader.onAfterAddingAll = ((items) => {
|
||||
this.onFileSelected.emit(items);
|
||||
});
|
||||
if (isUndefined(this.onBeforeUpload)) {
|
||||
this.onBeforeUpload = () => {return;};
|
||||
}
|
||||
this.uploader.onBeforeUploadItem = (item) => {
|
||||
if (item.url !== this.uploader.options.url) {
|
||||
item.url = this.uploader.options.url;
|
||||
}
|
||||
// Ensure the current XSRF token is included in every upload request (token may change between items uploaded)
|
||||
this.uploader.options.headers = [{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }];
|
||||
this.onBeforeUpload();
|
||||
this.isOverDocumentDropZone = observableOf(false);
|
||||
};
|
||||
if (hasValue(this.uploadProperties)) {
|
||||
this.uploader.onBuildItemForm = (item, form) => {
|
||||
form.append('properties', JSON.stringify(this.uploadProperties));
|
||||
};
|
||||
}
|
||||
this.uploader.onCompleteItem = (item: any, response: any, status: any, headers: any) => {
|
||||
// Check for a changed XSRF token in response & save new token if found (to both cookie & header for next request)
|
||||
// NOTE: this is only necessary because ng2-file-upload doesn't use an Http service and therefore never
|
||||
// triggers our xsrf.interceptor.ts. See this bug: https://github.com/valor-software/ng2-file-upload/issues/950
|
||||
const token = headers[XSRF_RESPONSE_HEADER.toLowerCase()];
|
||||
if (isNotEmpty(token)) {
|
||||
this.saveXsrfToken(token);
|
||||
this.uploader.options.headers = [{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }];
|
||||
}
|
||||
|
||||
if (isNotEmpty(response)) {
|
||||
const responsePath = JSON.parse(response);
|
||||
this.onCompleteItem.emit(responsePath);
|
||||
}
|
||||
};
|
||||
this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => {
|
||||
// Check for a changed XSRF token in response & save new token if found (to both cookie & header for next request)
|
||||
// NOTE: this is only necessary because ng2-file-upload doesn't use an Http service and therefore never
|
||||
// triggers our xsrf.interceptor.ts. See this bug: https://github.com/valor-software/ng2-file-upload/issues/950
|
||||
const token = headers[XSRF_RESPONSE_HEADER.toLowerCase()];
|
||||
if (isNotEmpty(token)) {
|
||||
this.saveXsrfToken(token);
|
||||
this.uploader.options.headers = [{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }];
|
||||
}
|
||||
|
||||
this.onUploadError.emit({ item: item, response: response, status: status, headers: headers });
|
||||
this.uploader.cancelAll();
|
||||
};
|
||||
this.uploader.onProgressAll = () => this.onProgress();
|
||||
this.uploader.onProgressItem = () => this.onProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when files are dragged on the base drop area.
|
||||
*/
|
||||
public fileOverBase(isOver: boolean): void {
|
||||
this.isOverBaseDropZone = observableOf(isOver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when files are dragged on the window document drop area.
|
||||
*/
|
||||
public fileOverDocument(isOver: boolean) {
|
||||
if (!isOver) {
|
||||
this.isOverDocumentDropZone = observableOf(isOver);
|
||||
}
|
||||
}
|
||||
|
||||
private onProgress() {
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure options passed contains the required properties.
|
||||
*
|
||||
* @param fileUploadOptions
|
||||
* The upload-files options object.
|
||||
*/
|
||||
private checkConfig(fileUploadOptions: any) {
|
||||
const required = ['url', 'authToken', 'disableMultipart', 'itemAlias'];
|
||||
const missing = required.filter((prop) => {
|
||||
return !((prop in fileUploadOptions) && fileUploadOptions[prop] !== '');
|
||||
});
|
||||
if (0 < missing.length) {
|
||||
throw new Error('UploadFiles: Argument is missing the following required properties: ' + missing.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save XSRF token found in response. This is a temporary copy of the method in xsrf.interceptor.ts
|
||||
* It can be removed once ng2-file-upload supports interceptors (see https://github.com/valor-software/ng2-file-upload/issues/950),
|
||||
* or we switch to a new upload library (see https://github.com/DSpace/dspace-angular/issues/820)
|
||||
* @param token token found
|
||||
*/
|
||||
private saveXsrfToken(token: string) {
|
||||
// Save token value as a *new* value of our client-side XSRF-TOKEN cookie.
|
||||
// This is the cookie that is parsed by Angular's tokenExtractor(),
|
||||
// which we will send back in the X-XSRF-TOKEN header per Angular best practices.
|
||||
this.cookieService.remove(XSRF_COOKIE);
|
||||
this.cookieService.set(XSRF_COOKIE, token);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user