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'>&nbsp;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">&nbsp;</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>

results matching ""

    No results matching ""