diff --git a/app/directives/progress-bar/progress-bar.directive.jade b/app/directives/progress-bar/progress-bar.directive.jade new file mode 100644 index 000000000..7fc962a9b --- /dev/null +++ b/app/directives/progress-bar/progress-bar.directive.jade @@ -0,0 +1,6 @@ +.progress-bar + .progress-bar__bar + .progress-bar__bar--completed + .progress-bar__summary + span(ng-bind="completed") + span % {{message}} \ No newline at end of file diff --git a/app/directives/progress-bar/progress-bar.directive.js b/app/directives/progress-bar/progress-bar.directive.js new file mode 100644 index 000000000..072eb368e --- /dev/null +++ b/app/directives/progress-bar/progress-bar.directive.js @@ -0,0 +1,29 @@ +(function() { + 'use strict'; + + angular.module('tcUIComponents').directive('progressBar', progressBar); + + progressBar.$inject = ['$timeout', '$parse']; + + function progressBar($timeout, $parse) { + return { + restrict: 'E', + templateUrl: 'directives/progress-bar/progress-bar.directive.html', + link: function(scope, element, attr) { + var model = $parse(attr.completed); + var msg = attr.message; + var progress = angular.element(element[0].querySelector('.progress-bar__bar--completed')); + + scope.$watch(model, function(newValue, oldValue) { + scope.completed = Math.round(newValue); + // console.log("Updating progress bar with " + scope.completed); + scope.message = msg; + progress.css('width', scope.completed + '%') + }); + }, + controller: ['$scope', function($scope) { + + }] + }; + } +})(); diff --git a/app/index.jade b/app/index.jade index e8476ccf4..0db7b9d19 100644 --- a/app/index.jade +++ b/app/index.jade @@ -17,6 +17,7 @@ html link(rel='stylesheet', href='../bower_components/intro.js/introjs.css') link(rel='stylesheet', href='../bower_components/angularjs-toaster/toaster.css') link(rel='stylesheet', href='../bower_components/react-select/dist/react-select.min.css') + link(rel='stylesheet', href='../bower_components/appirio-tech-ng-ui-components/dist/main.css') link(rel='stylesheet', href='../bower_components/fontawesome/css/font-awesome.css') link(rel='stylesheet', href='../bower_components/ng-notifications-bar/dist/ngNotificationsBar.min.css') link(rel='stylesheet', href='../bower_components/ngDialog/css/ngDialog.css') @@ -72,6 +73,7 @@ html link(rel="stylesheet", href="assets/css/directives/srm-tile.css") link(rel="stylesheet", href="assets/css/directives/skill-tile.css") link(rel="stylesheet", href="assets/css/directives/responsive-carousel.css") + link(rel="stylesheet", href="assets/css/directives/progress-bar.directive.css") link(rel="stylesheet", href="assets/css/directives/profile-widget.css") link(rel="stylesheet", href="assets/css/directives/page-state-header.directive.css") link(rel="stylesheet", href="assets/css/directives/ios-card.css") @@ -221,6 +223,7 @@ html script(src="directives/page-state-header/page-state-header.directive.js") script(src="directives/preventEventPropagation.directive.js") script(src="directives/profile-widget/profile-widget.directive.js") + script(src="directives/progress-bar/progress-bar.directive.js") script(src="directives/responsive-carousel/responsive-carousel.directive.js") script(src="directives/skill-tile/skill-tile.directive.js") script(src="directives/slideable.directive.js") diff --git a/app/services/submissions.service.js b/app/services/submissions.service.js index 5e7488d13..b027643fd 100644 --- a/app/services/submissions.service.js +++ b/app/services/submissions.service.js @@ -17,23 +17,24 @@ return service; - function getPresignedURL(body, files) { + function getPresignedURL(body, files, progressCallback) { console.log('Body of request for presigned url: ', body); return api.all('submissions').customPOST(body) .then(function(response) { console.log('POST/Presigned URL Response: ', response.plain()); - - uploadSubmissionFileToS3(response, response.data.files, files); + progressCallback.call(progressCallback, 'PREPARE', 100); + uploadSubmissionFileToS3(response, response.data.files, files, progressCallback); }) .catch(function(err) { console.log(err); $log.info('Error getting presigned url'); + progressCallback.call(progressCallback, 'ERROR', err); toaster.pop('error', 'Whoops!', 'There was an error uploading your submissions. Please try again later.'); }); } - function uploadSubmissionFileToS3(presignedURLResponse, filesWithPresignedURL, files) { + function uploadSubmissionFileToS3(presignedURLResponse, filesWithPresignedURL, files, progressCallback) { var promises = filesWithPresignedURL.map(function(fileWithPresignedURL) { var deferred = $q.defer(); @@ -42,6 +43,22 @@ xhr.open('PUT', fileWithPresignedURL.preSignedUploadUrl, true); xhr.setRequestHeader('Content-Type', fileWithPresignedURL.mediaType); + xhr.upload.addEventListener("progress", function(oEvent) { + if (oEvent.lengthComputable) { + var percentComplete = oEvent.loaded / oEvent.total; + // console.log("Completed " + percentComplete); + if (progressCallback && typeof progressCallback === 'function') { + progressCallback.call(progressCallback, 'UPLOAD', { + file: fileWithPresignedURL.preSignedUploadUrl, + progress: percentComplete*100 + }); + } + // ... + } else { + // Unable to compute progress information since the total size is unknown + } + }); + // xhr version of the success callback xhr.onreadystatechange = function() { var status = xhr.status; @@ -74,17 +91,18 @@ .then(function(response) { console.log('response from S3: ', response); console.log('response to use .save restnagular with: ', presignedURLResponse); - + progressCallback.call(progressCallback, 'UPLOAD', 100); // Update and start processing - updateSubmissionStatus(presignedURLResponse.plain()); + updateSubmissionStatus(presignedURLResponse.plain(), progressCallback); }) .catch(function(err) { + progressCallback.call(progressCallback, 'ERROR', err); console.log('error uploading to S3: ', err); }); } - function updateSubmissionStatus(body) { + function updateSubmissionStatus(body, progressCallback) { // Pass data from upload to S3 body.data.files.forEach(function(file) { file.status = 'UPLOADED'; @@ -93,24 +111,27 @@ return api.one('submissions', body.id).customPUT(body) .then(function(response) { $log.info('Successfully updated file statuses'); - recordCompletedSubmission(response.plain()); + recordCompletedSubmission(response.plain(), progressCallback); }) .catch(function(err) { $log.info('Error updating file statuses'); $log.error(err); + progressCallback.call(progressCallback, 'ERROR', err); }); } - function recordCompletedSubmission(body) { + function recordCompletedSubmission(body, progressCallback) { // Once all uploaded, make record and begin processing return api.one('submissions', body.id).customPOST(body, 'process') .then(function(response) { $log.info('Successfully made file record. Beginning processing'); console.log('response from process call: ', response); + progressCallback.call(progressCallback, 'FINISH', 100); }) .catch(function(err) { $log.info('Error in starting processing'); $log.error(err); + progressCallback.call(progressCallback, 'ERROR', err); }); } }; diff --git a/app/submissions/submit-file/submit-file.controller.js b/app/submissions/submit-file/submit-file.controller.js index 93b19ed84..b2e6fbd43 100644 --- a/app/submissions/submit-file/submit-file.controller.js +++ b/app/submissions/submit-file/submit-file.controller.js @@ -3,14 +3,21 @@ angular.module('tc.submissions').controller('SubmitFileController', SubmitFileController); - SubmitFileController.$inject = ['$stateParams', 'UserService', 'SubmissionsService', 'challengeToSubmitTo']; + SubmitFileController.$inject = ['$scope', '$stateParams', 'UserService', 'SubmissionsService', 'challengeToSubmitTo']; - function SubmitFileController($stateParams, UserService, SubmissionsService, challengeToSubmitTo) { + function SubmitFileController($scope, $stateParams, UserService, SubmissionsService, challengeToSubmitTo) { var vm = this; var files = {}; + var fileUploadProgress = {}; vm.urlRegEx = new RegExp(/^(http(s?):\/\/)?(www\.)?[a-zA-Z0-9\.\-\_]+(\.[a-zA-Z]{2,3})+(\/[a-zA-Z0-9\_\-\s\.\/\?\%\#\&\=]*)?$/); vm.rankRegEx = new RegExp(/^[1-9]\d*$/); vm.comments = ''; + vm.uploadProgress = 0; + vm.uploading = false; + vm.preparing = false; + vm.finishing = false; + vm.showProgress = false; + vm.errorInUpload = false; vm.formFonts = [{ id: 0, source: '', @@ -69,6 +76,7 @@ vm.setFileReference = setFileReference; vm.uploadSubmission = uploadSubmission; vm.createAnotherStockArtFieldset = createAnotherStockArtFieldset; + vm.cancelRetry = cancelRetry; activate(); @@ -128,6 +136,11 @@ } function uploadSubmission() { + vm.errorInUpload = false; + vm.uploadProgress = 0; + vm.fileUploadProgress = {}; + vm.showProgress = true; + vm.preparing = true; vm.submissionsBody.data.submitterComments = vm.comments; vm.submissionsBody.data.submitterRank = vm.submissionForm.submitterRank; @@ -161,7 +174,66 @@ } console.log('Body for request: ', vm.submissionsBody); - SubmissionsService.getPresignedURL(vm.submissionsBody, files); + SubmissionsService.getPresignedURL(vm.submissionsBody, files, updateProgress); + } + + /** + * Callback for updating submission upload process. It looks for different phases e.g. PREPARE, UPLOAD, FINISH + * of the submission upload and updates the progress UI accordingly. + */ + function updateProgress(phase, args) { + // for PREPARE phase + if (phase === 'PREPARE') { + // we are concerned only for completion of the phase + if (args === 100) { + vm.preparing = false; + vm.uploading = true; + console.log('Prapared'); + } + } else if (phase === 'UPLOAD') { + // if args is object, this update is about XHRRequest's upload progress + if (typeof args === 'object') { + var requestId = args.file; + var progress = args.progress; + if (!fileUploadProgress[requestId] || fileUploadProgress[requestId] < progress) { + fileUploadProgress[requestId] = progress; + } + var total = 0, count = 0; + for(var requestId in fileUploadProgress) { + var prog = fileUploadProgress[requestId]; + total += prog; + count++; + } + vm.uploadProgress = total / count; + // initiate digest cycle because this event (xhr event) is caused outside angular + $scope.$apply(); + } else { // typeof args === 'number', mainly used a s fallback to mark completion of the UPLOAD phase + vm.uploadProgress = args; + } + // start next phase when UPLOAD is done + if (vm.uploadProgress == 100) { + console.log('Uploaded'); + vm.uploading = false; + vm.finishing = true; + } + } else if (phase === 'FINISH') { + // we are concerned only for completion of the phase + if (args === 100) { + console.log('Finished'); + vm.finishing = false; + vm.showProgress = false; + + // TODO redirect to submission listing / challenge details page + } + } else { // assume it to be error condition + console.log("Else: " + phase); + vm.errorInUpload = true; + } + } + + function cancelRetry() { + vm.showProgress = false; + // TODO redirect to submission listing / challenge details page } } })(); diff --git a/app/submissions/submit-file/submit-file.jade b/app/submissions/submit-file/submit-file.jade index 27d15aae3..a8af832cc 100644 --- a/app/submissions/submit-file/submit-file.jade +++ b/app/submissions/submit-file/submit-file.jade @@ -159,3 +159,25 @@ label(for="agree-to-terms") I understand and agree button.tc-btn.tc-btn-secondary(type="submit", ng-disabled="submissionForm.$invalid") Submit + +modal.transition(show="vm.showProgress", background-click-close="true", style="background-color:white;") + .upload-progress(ng-class="{'upload-progress--error': vm.errorInUpload}") + .upload-progress__title + p Uploading submission for + p.upload-progress-title__challenge-name [Challenge name] + img.upload-progress__image(ng-src="/images/robot.svg", ng-hide="vm.errorInUpload") + img.upload-progress__image--error(ng-src="/images/robot-embarresed.svg", ng-show="vm.errorInUpload") + p.upload-progress__message(ng-hide="vm.errorInUpload") Hey, your work is AWESOME! Please don’t close the window while I’m working, you’ll loose all files! + p.upload-progress__message--error(ng-show="vm.errorInUpload") Oh, that’s embarrassing! The file couldn’t be uploaded, I’m so sorry. + + progress-bar.upload-progress__progress-bar(completed="vm.uploadProgress", message="of 3 files uploaded") + + .upload-progress__preparing(ng-show="vm.preparing && !vm.errorInUpload") + span Preparing... + .upload-progress__finishing(ng-show="vm.finishing && !vm.errorInUpload") + span Finishing... + .upload-progress__error(ng-show="vm.errorInUpload") + span File upload failed + .upload-progress__error-action(ng-show="vm.errorInUpload") + button.tc-btn.tc-btn-s.tc-btn-ghost(type="button", ng-click="vm.cancelRetry()") Cancel + button.tc-btn.tc-btn-s.tc-btn-secondary(type="button", ng-click="submissionForm.$valid && vm.uploadSubmission()") Try Again \ No newline at end of file diff --git a/app/submissions/submit-file/submit-file.spec.js b/app/submissions/submit-file/submit-file.spec.js index d3567389a..d24961aca 100644 --- a/app/submissions/submit-file/submit-file.spec.js +++ b/app/submissions/submit-file/submit-file.spec.js @@ -2,6 +2,7 @@ describe('Submit File Controller', function() { var controller; var vm; + var scope; var mockChallenge = { challenge: { @@ -21,13 +22,16 @@ describe('Submit File Controller', function() { beforeEach(function() { bard.appModule('tc.submissions'); - bard.inject(this, '$controller'); + bard.inject(this, '$controller', '$rootScope'); + + scope = $rootScope.$new(); }); bard.verifyNoOutstandingHttpRequests(); beforeEach(function() { controller = $controller('SubmitFileController', { + $scope: scope, UserService: userService, challengeToSubmitTo: mockChallenge }); @@ -37,4 +41,10 @@ describe('Submit File Controller', function() { it('should exist', function() { expect(vm).to.exist; }); + + describe('updateProgress ', function() { + it('should update PREPARE phase end ', function() { + + }); + }); }); diff --git a/assets/css/directives/progress-bar.directive.scss b/assets/css/directives/progress-bar.directive.scss new file mode 100644 index 000000000..e7503e240 --- /dev/null +++ b/assets/css/directives/progress-bar.directive.scss @@ -0,0 +1,21 @@ +@import 'topcoder/tc-includes'; + +.progress-bar { + .progress-bar__bar { + background-color: $gray-light; + height: 10px; + + .progress-bar__bar--completed { + background-color: $dark-blue; + height: 100%; + } + } + + .progress-bar__summary { + @include font-with-weight('Sofia Pro', 500); + font-size: 12px; + line-height: 14px; + color: $accent-gray-dark; + margin-top: 20px; + } +} \ No newline at end of file diff --git a/assets/css/submissions/submit-file.scss b/assets/css/submissions/submit-file.scss index 5eb8ee3f8..60a507f4b 100644 --- a/assets/css/submissions/submit-file.scss +++ b/assets/css/submissions/submit-file.scss @@ -45,6 +45,76 @@ margin-top: 10px; } +modal { + .close { + .icon.cross { + background-image: url(/images/x-mark-gray.svg); + background-size: 25px; + } + } +} +.upload-progress { + display:flex; + flex-direction:column; + align-items: center; + + .upload-progress__title { + text-transform: uppercase; + @include font-with-weight('Sofia Pro', 500); + font-size: 24px; + line-height: 30px; + max-width: 355px; + display: flex; + flex-direction: column; + align-items: center; + } + + .upload-progress__image, + .upload-progress__image--error { + margin-top: 60px; + } + + .upload-progress__message, + .upload-progress__message--error { + @include font-with-weight('Merriweather Sans', 400); + font-size: 15px; + line-height: 19px; + color: $accent-gray; + margin-top: 40px; + } + + .upload-progress__progress-bar { + margin-top: 30px; + width: 100%; + max-width: 760px; + } + + .upload-progress__preparing, + .upload-progress__finishing, + .upload-progress__error { + @include font-with-weight('Sofia Pro', 500); + font-size: 12px; + line-height: 14px; + color: $accent-gray-dark; + } + + .upload-progress__error { + color: $error; + } + + .upload-progress__error-action { + margin-top: 40px; + } + + &.upload-progress--error { + .upload-progress__progress-bar { + .progress-bar__bar--completed { + background-color: $error; + } + } + } +} + tc-form-fonts { .fieldset { max-width: 500px; diff --git a/assets/images/robot-embarresed.svg b/assets/images/robot-embarresed.svg new file mode 100755 index 000000000..e61dbeeff --- /dev/null +++ b/assets/images/robot-embarresed.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bower.json b/bower.json index f694eb313..44074f065 100644 --- a/bower.json +++ b/bower.json @@ -40,7 +40,7 @@ "angular-xml": "~2.1.1", "angularjs-toaster": "~0.4.15", "appirio-tech-ng-iso-constants": "git@github.com:appirio-tech/ng-iso-constants#~1.0.5", - "appirio-tech-ng-ui-components": "1.x.x", + "appirio-tech-ng-ui-components": "appirio-tech/ng-ui-components#bower-wiredep-fix", "d3": "~3.5.6", "fontawesome": "~4.3.0", "jstzdetect": "~1.0.6",