File Upload Panel
A component for a UI for uploading a file on SciDap:
A simple guide to help let you tweak this component however you like.
The selector for this component is "sd-upload-panel"
Inputs:
- title
- A string for the title
- @optional
storagePath
- A string that denotes the relative path to store this file.
- See note below in section titled "How storagePath Works"
@requires: Has a leading slash and is a existing path.
@optional - Default will store in homepath.
fileCollection
- The file collection to insert the given file to.
- @required - Code assumes this is given, so it will break at upload start if it is not.
Outputs:
- onStatusChange
- Every time a state change occurs in the upload progress (see below), an event will be triggered that carries the value of the state that the object is _entering into _as a string literal.
- Possible Values of $event:
- "PICK_FILE"
- "CONFIRM_PICK"
- "UPLOADING"
- "DONE"
- "ERROR"
Code:
sd-upload-panel.component.ts
import {Component, EventEmitter, Input, NgZone, Output} from "@angular/core";
import template from "./sd-upload-panel.html"
import style from "./sd-upload-panel.scss"
import {FileObj, FilesCollection} from "meteor/ostrio:files";
import {MdDialog, MdDialogRef} from "@angular/material";
import {CancelConfirmationDialog} from "./sd-cancel-upload-dialog.component";
export enum UploadStatus{
PICK_FILE = 0,
CONFIRM_PICK = 1,
UPLOADING = 2,
DONE = 3,
ERROR = 4
}
type UploadStatusString = "PICK_FILE" | "CONFIRM_PICK" | "UPLOADING" | "DONE" | "ERROR";
@Component({
selector: 'sd-upload-panel',
template,
styles: [style]
})
export class SDUploadPanel{
//Useful Constants
private tooltipDelay:number = 500; //ms
//Workflow Mangement
private _currentStatus:UploadStatus = UploadStatus.PICK_FILE;
//Formatting
private currentFileName:string = 'Choose a file...';
private loadProgress:number = 0; //%
private loadFraction:string="/";
private barColor:string = "primary";
//File Upload
private currentFile; //File or FileObj
private currentUpload; //FileInsert Object (see "insert()" in ostrio:files docs)
private errorMessage:string = "";
/**
* title - The content of the displayed header. Is just string for now.
*/
@Input('title')
sectionTitle:string;
/**
* storagePath - The Relative storage path of where you want the file stored.
* If not provided, uploaded file will simply be placed into home directory
*/
@Input('storagePath')
storagePath:string;
/**
* fileCollection - The given ostrio:files collection to insert the uploaded file into.
*/
@Input('fileCollection')
currentCollection:FilesCollection;
/**
* Gives the current upload status as one of the following strings:
* "PICK_FILE" | "CONFIRM_PICK" | "UPLOADING" | "DONE" | "ERROR"
* @type {EventEmitter<UploadStatusString>}
*/
@Output('onStatusChange')
statusStringEmitter:EventEmitter<UploadStatusString> = new EventEmitter<UploadStatusString>();
/*
Prepare dialog and zone, and set state during init.
*/
constructor(private _zone:NgZone, public dialog: MdDialog){}
ngOnInit(){this.pickNewFile();}
/**
* Given an array of state values, it will return true if the current state is contained.
* @param states
* @returns {boolean}
*/
checkStatus(states:UploadStatus[]|number[]){
for(let i=0;i<states.length;i++){
if(states[i]==this._currentStatus){return true; }
}
return false;
}
/**
* Both sets the current status and emits the string literal on the output stream.
* @param newStatus
*/
private setStatus(newStatus:UploadStatus){
this._currentStatus = newStatus;
switch(newStatus){
case UploadStatus.PICK_FILE:
this.statusStringEmitter.emit("PICK_FILE");
this.loadProgress = 0;
this.currentFileName = "Choose a file...";
break;
case UploadStatus.CONFIRM_PICK:
this.statusStringEmitter.emit("CONFIRM_PICK");
break;
case UploadStatus.UPLOADING:
this.statusStringEmitter.emit("UPLOADING");
this.barColor = "primary";
break;
case UploadStatus.DONE:
this.statusStringEmitter.emit("DONE");
this.barColor = "accent";
break;
case UploadStatus.ERROR:
this.statusStringEmitter.emit("ERROR");
this.barColor = "warn";
this.currentFileName = "Choose a new file...";
break;
default:
throw 'Illegal Status';
}
}
/*
State methods:
Theses methods perform the neccessary entry actions with the associated state.
*/
/**
* Transitions to PICK_FILE state.
*/
pickNewFile(){
this.setStatus(UploadStatus.PICK_FILE);
}
/**
* Transitions to CONFIRM_PICK state.
* @param $event - File from file input.
*/
prepareFile($event:File){
this.currentFileName = $event.name;
this.currentFile = $event;
this.setStatus(UploadStatus.CONFIRM_PICK);
}
/**
* Transitions to UPLOADING state. Also prepares enter actions towards ERROR and DONE states,
* and prepares the associated events.
*/
startUpload(){
if(!!this.currentFile){
//@TODO: Consider stripping metadata.
if(this.currentFile.meta && !this.currentFile.meta.storagePath && this.storagePath){
this.currentFile.meta.storagePath = this.storagePath;
} else if(!this.currentFile.meta){
this.currentFile.meta = {storagePath:this.storagePath};
}
console.log(this.currentFile.meta);
this.setStatus(UploadStatus.UPLOADING);
this.currentUpload = this.currentCollection.insert({
file:this.currentFile,
onProgress: (progress,fileObj) => {this._zone.run(()=>{
this.loadProgress = progress;
this.loadFraction = this.getFileSizeString(this.currentFile.size * (this.loadProgress/100))
+" / "+this.getFileSizeString(this.currentFile.size);
});},
onUploaded: (err,fileObj:FileObj) => {
this._zone.run(()=>{
if(err){this.errorMessage=err.toString();throw err;}
else{
console.log("File Uploaded: " + fileObj.name);
this.setStatus(UploadStatus.DONE);
}});
},
onAbort: fileData => {this.errorMessage = "Upload Aborted!";console.log("Upload of File Aborted");},
meta: this.currentFile.meta
});
}
}
/**
* Asks user to confirm cancel then performs exit actions towards ERROR state.
*/
cancelUpload(){
this.dialog.open(CancelConfirmationDialog).afterClosed().subscribe(result=>{
if(result === "abort"){
this.currentUpload.abort();
this.setStatus(UploadStatus.ERROR);
}
});
}
/**
* A method that takes a count of bytes then returns an appropertly sized rounded string with label.
* @param bytes
* @requires bytes:number < 1000Pb
* @returns {string}
*/
getFileSizeString(bytes:number):string{
let roundedBytes = Math.floor(bytes);
let labels:string[] = ["b","Kb","Mb","Gb","Tb","Pb"];
let numberLength:number = roundedBytes.toString().length;
let orderOfMagnitude = Math.floor((numberLength-1)/3);
//Rounds adjusted value to one decimal place;
let adjustedAmount = Math.round((roundedBytes / Math.pow(10,3*orderOfMagnitude)*10))/10;
return adjustedAmount + labels[orderOfMagnitude];
}
}
sd-upload-panel.html
<h5 class="panel-title">{{sectionTitle}}</h5>
<!-- Step States: 0 - Pick File, 1 - Confirm Pick, 2 - Uploading, 3 - Done, 4 - Error-->
<div class="panel-container">
<div layout="row" layout-align="start center" class="button-panel">
<!--PICK FILE-->
<!--Upload Button and FileName-->
<td-file-input (select)="prepareFile($event)" [disabled]="checkStatus([2,3])"
(upload)="startUpload($event)" (cancel)="pickNewFile()" color="primary">
<img src="/images/static_white_gear.gif" class="image-to-icon"
[class.rotate-gear-animation] = "checkStatus([2])"/>
<span>{{currentFileName}}</span>
<span *ngIf="checkStatus([1])">({{getFileSizeString(currentFile.size)}})</span>
</td-file-input>
<!--CONFIRM UPLOAD-->
<!--Start Upload-->
<button md-icon-button color="accent" *ngIf="checkStatus([0,1])"
mdTooltip="Start Upload" [mdTooltipShowDelay]="tooltipDelay"
(click) ="startUpload()" [disabled]="!checkStatus([1])">
<i class="material-icons">check_circle</i>
</button>
<!--Pick New File-->
<button md-icon-button color="accent" *ngIf="checkStatus([0,1,4])"
mdTooltip="Pick Another File" [mdTooltipShowDelay]="tooltipDelay"
(click) ="pickNewFile()" [disabled]="!checkStatus([1,4])">
<i class="material-icons">attach_file</i>
</button>
<!--UPLOADING-->
<!--Cancel Upload Button-->
<button md-icon-button *ngIf="checkStatus([2])"
color="warn" mdTooltip="Stop Upload"
[mdTooltipShowDelay]="tooltipDelay" (click) ="cancelUpload()">
<i class="material-icons">cancel</i>
</button>
<!--Upload Progress-->
<span *ngIf="checkStatus([2])">Upload Progress: {{loadProgress}}% --- ({{loadFraction}})</span>
<span *ngIf="checkStatus([3])" class='success'> Done! </span>
<span *ngIf="checkStatus([4])" class='warn'>{{errorMessage}}</span>
<!--Invisible Button to keep alignment consistant-->
<!--TODO: Figure out why buttons don't play nice with flex-->
<button md-icon-button [disabled]="true"> </button>
</div>
<!--Progress Bar-->
<md-progress-bar class="upload-progress" mode="determinate"
[value]="loadProgress" [color]="barColor"
*ngIf="checkStatus([2,3,4])"></md-progress-bar>
</div>
sd-upload-panel.scss
@import '../../../../../client/app';
/*
Useful Constants
*/
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$background : 50;
$panel-height: 65px;
/*
Utility Classes
*/
.primary{
color:mat-color($primary);
}
.accent{
color:mat-color($accent);
}
.success{
color:#259b24;
}
.warn{
color:mat-color($warn);
}
.image-to-icon{
height:24px; //Default height for material icon.
}
/*
Component Formatting
*/
.panel-container{
$border-color: mat-color($accent,$background);
border-top: solid 1px $border-color;
border-bottom: solid 1px $border-color;
padding-bottom:0;
min-height: $panel-height;
margin: 5px 0 5px 0;
position:relative;
}
.button-panel{
display:flex;
flex-wrap: wrap;
padding: 5px;
}
.upload-progress{
position:absolute;
bottom:0;
}
.panel-title{
margin:20px 5px 0 5px;
padding: 5px;
padding-bottom: 0;
}
button{margin:5px;}
/*
Animations
*/
//Gear Animation
@keyframes rotate-gear {
from{
-ms-transform: rotate(0); /* IE 9 */
-webkit-transform: rotate(0); /* Chrome, Safari, Opera */
transform: rotate(0);
}
to{
$rotation-measurement:45deg;
-ms-transform: rotate($rotation-measurement); /* IE 9 */
-webkit-transform: rotate($rotation-measurement); /* Chrome, Safari, Opera */
transform: rotate($rotation-measurement);
}
}
.rotate-gear-animation{
animation-name: rotate-gear;
animation-timing-function: linear;
animation-duration: 0.3s;
animation-iteration-count: infinite;
}
sd-upload-panel.module.ts
import {NgModule} from "@angular/core";
import {
MdLineModule, MdRippleModule, MdCommonModule, MdProgressBarModule, MdCardModule,
MdProgressSpinnerModule, MdButtonModule, MdTooltipModule,MdDialogModule
} from '@angular/material';
import {SDUploadPanel} from "./sd-upload-panel.component";
import {CovalentFileModule} from "@covalent/core";
import {CommonModule} from "@angular/common";
import {BrowserModule} from "@angular/platform-browser";
import {CancelConfirmationDialog} from "./sd-cancel-upload-dialog.component";
import {SciDAPPlatformAppModule} from "../../platform.module";
@NgModule({
imports:[
MdLineModule, MdRippleModule, MdCommonModule,MdProgressBarModule,MdCardModule,CovalentFileModule,
MdProgressSpinnerModule,MdButtonModule,MdTooltipModule,CommonModule,BrowserModule,MdDialogModule
],
declarations:[
SDUploadPanel,CancelConfirmationDialog
],
exports:[
SDUploadPanel
],
entryComponents: [ CancelConfirmationDialog ]
})
export class SDUploadPanelModule{}
sd-cancel-upload-dialog.component.ts
import {MdDialog, MdDialogRef} from "@angular/material";
import {Component} from "@angular/core";
import template from "./sd-cancel-upload-dialog.html"
@Component({
template,
selector: "sd-cancel-upload-dialog"
})
export class CancelConfirmationDialog{
constructor(public dialogRef: MdDialogRef<CancelConfirmationDialog>, public dialog: MdDialog) {}
}
sd-cancel-upload-dialog.html
<h1 md-dialog-title>Abort Upload</h1>
<md-dialog-content>Are you sure you want to abort this upload?</md-dialog-content>
<md-dialog-actions>
<button md-raised-button md-dialog-close="abort" color="warn">Yes, abort</button>
<button md-button md-dialog-close>Nevermind...</button>
</md-dialog-actions>