import { ReplaySubject, Subject, timer as observableTimer } from "rxjs";
import { takeUntil } from "rxjs/operators";
import {
ChangeDetectorRef,
Component,
HostListener,
Input,
OnDestroy,
OnInit,
} from "@angular/core";
import { FileItem, FileUploader } from "ng2-file-upload";
import { HttpXsrfTokenExtractor } from "@angular/common/http";
import {
WidgetComponent,
WidgetConfiguration,
WidgetConfigure,
WidgetInput,
WidgetOutput,
} from "../widget.metadata";
import {
LocalStorageEntry,
LocalStorageService,
} from "../../../../app/shared/components/local-storage/local-storage.service";
import {
DeletionMode,
Scope,
} from "../../../../app/shared/components/local-storage/local-storage-constants";
import {
getOrDefault,
throwErrorIfUndefined,
WidgetConfig,
} from "../../../../app/shared/widgets/widget.configuration";
import { WidgetframeService } from "../../../../app/shared/widgets/widgetframe/widgetframe.service";
import { NgUnsubscribe } from "../../../../app/shared/ng-unsubscribe";
import { AppContext } from "../../../../app/shared/components/app-context/app.context";
import { CustomNotificationService } from "../../../../app/shared/components/notification/customnotification.service";
import { HalService } from "../../../../app/shared/components/hal/hal.service";
import * as uriTemplates_ from "uri-templates";
import { UtilService } from "../../components/util";
declare var contextPath: string;
const uriTemplates = uriTemplates_;
export interface FileUploadConfiguration {
/**
* Prefix for local storage. This is used for the done and error list. If the prefix is empty no local storage recovery will be used
*/
localStorageKey: string;
/**
* Uri of rest endpoint to create a ipim task for the upload process (required).
*/
taskUri: string;
/**
* Uri of rest endpoint to upload bytes of a file (required).
*/
uploadUri: string;
/**
* Uri of rest endpoint to finish one file upload (required).
*/
finishUri: string;
/**
* Base uri of rest endpoint to remove file from task (required).
* Example: "/api/apps/myapp/upload/task"
* This base uri will be extended by the file name (base64 encoded) to sth. like this:
* "/api/apps/myapp/upload/task/eW91cmZpbGUuanBn"
*/
removeUri: string;
/**
* Max. file size in bytes
* Example: 64000000 (for 64 MB)
*/
fileSizeLimit?: number | string;
/**
* A list of allowed MIME types. If the list is empty or null, all MIME types are allowed.
* Example: ['image/jpeg']
*/
allowedTypes?: string[];
/**
* A list of forbidden MIME types. If the list is empty or null, no MIME type is forbidden.
* Example: ['application/pdf', 'application/javascript']
*/
forbiddenTypes?: string[];
/**
* Label for the upload section (queue)
* Default: 'fileupload.panel.upload'
*/
uploadLabel?: string;
/**
* Label for the done section
* Default: 'fileupload.panel.done'
*/
doneLabel?: string;
/**
* Label for the error section
* Default: 'fileupload.panel.error'
*/
errorLabel?: string;
/**
* Text for the dropzone on mouseover
* Default: 'fileupload.overlay.text'
*/
dropZoneText?: string;
/**
* The height of this component. Will be the size of the full page -180px if unset
*/
height?: string;
/**
* Title of the info-dialog. Should be a translation-key
*/
infoTitle?: string;
/**
* Text of the info-dialog. Should be a translation-key
*/
infoText?: string;
}
export interface FileUploadInfo {
title: any;
size: any;
uri?: any;
creationDate: any;
type: any;
error?: any;
}
@WidgetComponent("nm-file-upload")
@Component({
selector: "nm-file-upload",
templateUrl: "./file-upload.component.html",
styleUrls: ["./file-upload.component.scss"],
host: {
"(window:resize)": "setContainerHeight()",
},
})
export class FileUploadComponentWidget implements OnInit, OnDestroy {
@Input("visible") visible: boolean;
@WidgetInput("clear")
public clear: Subject<void> = new ReplaySubject<void>(1);
@WidgetInput("upload")
public upload: Subject<void> = new ReplaySubject<void>(1);
@WidgetInput("taskUri")
public taskUriSubject: Subject<void> = new ReplaySubject<void>(1);
@WidgetOutput("queuesize")
public queuesize: Subject<any> = new Subject<any>();
@WidgetOutput("uploadFinished")
public uploadFinished = new Subject<any>();
public localStorageDone: LocalStorageEntry;
public localStorageError: LocalStorageEntry;
public uploader: FileUploader;
public hasBaseDropZoneOver: Boolean = false;
public action;
private uploadSystem: any = "";
@WidgetConfiguration()
public configuration: WidgetConfig<FileUploadConfiguration>;
public uploadLabel: string;
public doneLabel: string;
public errorLabel: string;
public dropZoneText: string;
public done: FileUploadInfo[] = [];
public error: FileUploadInfo[] = [];
public _containerHeight: string;
private uploadUri;
private taskUri;
private finishUri;
private removeUri;
private fileSizeLimit;
private unsubscribe = NgUnsubscribe.create();
public fileOverBase(e: any): void {
this.hasBaseDropZoneOver = e;
}
constructor(
private _halService: HalService,
private _notificationService: CustomNotificationService,
private changeDetector: ChangeDetectorRef,
private localStorageService: LocalStorageService,
private widgetframeService: WidgetframeService,
private appContext: AppContext,
private xsrfTokenExtractor: HttpXsrfTokenExtractor
) {}
@WidgetConfigure()
protected configureWidget(
configuration: WidgetConfig<FileUploadConfiguration>
) {
this.uploadUri = throwErrorIfUndefined(
"uploadUri",
this.configuration.configuration
);
this.finishUri = throwErrorIfUndefined(
"finishUri",
this.configuration.configuration
);
this.taskUri = throwErrorIfUndefined(
"taskUri",
this.configuration.configuration
);
this.removeUri = throwErrorIfUndefined(
"removeUri",
this.configuration.configuration
);
this.uploadLabel = getOrDefault(
this.configuration.configuration.uploadLabel,
"fileupload.panel.upload"
);
this.doneLabel = getOrDefault(
this.configuration.configuration.doneLabel,
"fileupload.panel.done"
);
this.errorLabel = getOrDefault(
this.configuration.configuration.errorLabel,
"fileupload.panel.error"
);
this.dropZoneText = getOrDefault(
this.configuration.configuration.dropZoneText,
"fileupload.overlay.text"
);
if (this.configuration.configuration.localStorageKey) {
this.localStorageDone = this.localStorageService.getLocalStorageEntry(
this.configuration.configuration.localStorageKey + "-done",
Scope.GLOBAL,
DeletionMode.NEVER
);
this.localStorageError = this.localStorageService.getLocalStorageEntry(
this.configuration.configuration.localStorageKey + "-error",
Scope.GLOBAL,
DeletionMode.NEVER
);
if (this.localStorageDone.exists()) {
this.done = JSON.parse(this.localStorageDone.value);
}
if (this.localStorageError.exists()) {
this.error = JSON.parse(this.localStorageError.value);
}
}
const fileSizeLimit = getOrDefault(
this.configuration.configuration.fileSizeLimit,
null
);
this.fileSizeLimit = UtilService.dataSizeToBytes(fileSizeLimit);
}
ngOnInit() {
this.setContainerHeight();
this.clear
.asObservable()
.pipe(takeUntil(this.unsubscribe))
.subscribe(() => {
this.done = [];
this.error = [];
if (this.localStorageDone) {
this.localStorageDone.value = JSON.stringify(this.done);
}
if (this.localStorageError) {
this.localStorageError.value = JSON.stringify(this.error);
}
this.changeDetector.detectChanges();
});
this.upload
.asObservable()
.pipe(takeUntil(this.unsubscribe))
.subscribe(() => {
document.getElementById("hidden-file-upload").click();
});
this.taskUriSubject
.asObservable()
.pipe(takeUntil(this.unsubscribe))
.subscribe((uri) => (this.taskUri = uri));
this.uploader = new FileUploader({ url: this.uploadUri });
this.uploader.onAfterAddingAll = (fileItems: any[]) => {
let filenames = [];
fileItems.forEach((fileItem) => {
let result = this.validate(fileItem.file);
if (result === true) {
filenames.push(fileItem.file.name);
} else {
this.onError(fileItem, {
title: "fileupload.validation.error",
message: "fileupload.validation.error." + result,
creationDate: new Date(),
});
}
});
if (filenames.length > 0) {
this.widgetframeService
.postData(this.taskUri, filenames)
.subscribe((response) => {
if (typeof Worker !== "undefined") {
const worker = new Worker(
contextPath + "/assets/file-upload.worker.js",
{ type: "module" }
);
worker.onmessage = ({ data }) => {
if (data.type === "progress") {
this.uploader.onProgressItem(
fileItems[data.file],
data.progress
);
} else if (data.level === "ERROR") {
this.uploader.onErrorItem(
fileItems[data.file],
data,
data.status,
null
);
} else if (data.level === "SUCCESS") {
this.uploader.onSuccessItem(
fileItems[data.file],
data,
data.status,
null
);
}
};
let files = fileItems.map((f) => f._file);
worker.postMessage({
xsrf: this.xsrfTokenExtractor.getToken(),
sessionId: getOrDefault(
this.appContext ? this.appContext.sessionId : "",
""
),
files: files,
contextPath: contextPath,
uploadUri: this.uploadUri,
finishUri: this.finishUri,
});
} else {
this.uploader.uploadAll();
this.queuesize.next(this.uploader.queue.length);
}
});
}
};
this.uploader.onAfterAddingFile = (fileItem: any) => {
this.fileOverBase(false);
fileItem.creationDate = Date.now();
};
this.uploader.onSuccessItem = (
item: any,
response: any,
status: any,
headers: any
) => {
item.isUploading = false;
this.uploader.removeFromQueue(item);
if (response) {
this.uploadFinished.next({ item, response, headers });
let responseObject = response;
let uri = responseObject.uri;
//If this is an absolute uri (no leading /) than take the full uri else add contextpath
if (uri !== "hotfolder" && responseObject.uri[0] === "/") {
uri = contextPath + responseObject.uri;
}
this.done.unshift({
title: responseObject.fileName,
size: responseObject.fileSize,
uri: uri,
creationDate: responseObject.creationDate,
error: responseObject.message,
type: item.file.type,
});
}
let timer = observableTimer(1);
timer.subscribe((t) => {
this.changeDetector.detectChanges();
});
if (this.localStorageDone) {
this.localStorageDone.value = JSON.stringify(this.done);
}
this.queuesize.next(this.uploader.queue.length);
};
this.uploader.onErrorItem = (
item: any,
response: any,
status: any,
headers: any
) => {
item.isError = true;
if (status === 500 || status === 0) {
let uri = this.removeUri + "/" + btoa(item.file.name);
this.widgetframeService.deleteData(uri).subscribe((response) => {
this.onError(item, response);
});
} else {
this.onError(item, response);
}
};
this.uploader.onProgressItem = (fileItem: FileItem, progress: any) => {
fileItem.progress = progress;
fileItem.isUploading = true;
this.changeDetector.detectChanges();
};
}
onError(item: any, response: any) {
item.isUploading = false;
this.uploader.removeFromQueue(item);
this.error.unshift({
title: item.file.name,
size: item.file.size,
creationDate: item.creationDate,
error: response.message,
type: item.file.type,
});
let timer = observableTimer(1);
timer.subscribe((t) => {
this.changeDetector.detectChanges();
});
if (this.localStorageError) {
this.localStorageError.value = JSON.stringify(this.error);
}
this.queuesize.next(this.uploader.queue.length);
}
validate(file: any) {
const type = file.type;
const size = file.size;
if (
this.configuration.configuration.allowedTypes &&
this.configuration.configuration.allowedTypes.length > 0 &&
this.configuration.configuration.allowedTypes.indexOf(type) === -1
) {
return "filetype";
}
if (
this.configuration.configuration.forbiddenTypes &&
this.configuration.configuration.forbiddenTypes.length > 0 &&
this.configuration.configuration.forbiddenTypes.indexOf(type) !== -1
) {
return "filetype";
}
if (this.fileSizeLimit && size > this.fileSizeLimit) {
return "filesize";
}
return true;
}
onSubmit() {
event.preventDefault();
}
setContainerHeight() {
if (this.configuration.configuration.height) {
this._containerHeight = this.configuration.configuration.height;
return;
}
let height =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;
this._containerHeight = String(height - 180) + "px";
}
dragenter(event) {
if (event.target.className === "file-drop-upload") {
event.stopImmediatePropagation();
event.stopPropagation();
this.fileOverBase(true);
}
}
dragleave(event) {
if (event.target.className === "file-drop-upload nv-file-over") {
event.stopImmediatePropagation();
event.stopPropagation();
this.fileOverBase(false);
}
}
get containerHeight() {
return this._containerHeight;
}
@HostListener("window:beforeunload", ["$event"])
onUnload($event: any) {
if (this.uploader.queue.length > 0) {
$event.returnValue = true;
}
}
ngOnDestroy(): void {
this.unsubscribe.destroy();
}
}