diff --git a/package.json b/package.json index 68f8bf6..bfbb6fb 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "angular": "^1.5.0", "angular-animate": "^1.5.0", "angular-aria": "^1.5.0", - "angular-breadcrumb": "^0.4.1", "angular-material": "^1.0.5", "angular-messages": "^1.5.0", "angular-translate": "^2.9.2", @@ -49,6 +48,7 @@ "javascript-natural-sort": "^0.7.1", "lodash": "^4.5.1", "material-design-icons": "^2.2.0", + "ng-file-upload": "^12.0.4", "satellizer": "^0.14.0" }, "devDependencies": { diff --git a/src/components/bucket/bucket.controller.js b/src/components/bucket/bucket.controller.js index 766be3c..c3fb2c7 100644 --- a/src/components/bucket/bucket.controller.js +++ b/src/components/bucket/bucket.controller.js @@ -1,8 +1,8 @@ export default class BucketController { /** @ngInject */ - constructor($scope, $bucket) { + constructor($scope, $bucket, $state, $breadcrumb) { Object.assign(this, { - $scope, $bucket, + $scope, $bucket, $state, }); this.$scope.$watch( @@ -10,10 +10,15 @@ export default class BucketController { newVal => Object.assign(this, newVal) , true); + $breadcrumb.initPaths(); this.$bucket.getBuckets(); } createBucket($event) { this.$bucket.createDialog($event); } + + selectBucket(bucket) { + this.$state.go('file', { path: bucket }); + } } diff --git a/src/components/bucket/bucket.html b/src/components/bucket/bucket.html index fd50d39..0044eeb 100644 --- a/src/components/bucket/bucket.html +++ b/src/components/bucket/bucket.html @@ -12,14 +12,13 @@ - + info_outline - - + diff --git a/src/components/bucket/bucket.js b/src/components/bucket/bucket.js index 1c31f17..1721a52 100644 --- a/src/components/bucket/bucket.js +++ b/src/components/bucket/bucket.js @@ -14,9 +14,6 @@ const route = $stateProvider => { controllerAs: 'bucket', template: BucketTemplate, onEnter: $nav => $nav.setTypeToBucket(), - ncyBreadcrumb: { - label: 'All Buckets ( {{ bucket.data.length }} )', - }, }); }; diff --git a/src/components/bucket/bucket.service.js b/src/components/bucket/bucket.service.js index 6fee2d9..2720db6 100644 --- a/src/components/bucket/bucket.service.js +++ b/src/components/bucket/bucket.service.js @@ -1,13 +1,13 @@ import { element } from 'angular'; -import natural from 'javascript-natural-sort'; +import { sortByName } from '../../utils/sort'; import BucketCreateController from './create/create.controller'; import BucketCreateTemplate from './create/create.html'; export default class BucketService { /** @ngInject */ - constructor($fetch, $toast, $mdDialog) { + constructor($fetch, $toast, $mdDialog, $breadcrumb) { Object.assign(this, { - $fetch, $toast, $mdDialog, + $fetch, $toast, $mdDialog, $breadcrumb, }); this.initState(); @@ -73,20 +73,6 @@ export default class BucketService { this.resetCheckBucketState(); } - /** - * Natural sort for the specified object key. - * - * @param {Object} a - * @param {Object} b - * @return {Integer} - */ - sortByName(a, b) { - const x = a.Name; - const y = b.Name; - - return natural(x, y); - } - /** * Call the bucket list API and modify the state of service. * @@ -99,13 +85,14 @@ export default class BucketService { this.$fetch.post('/v1/bucket/list') .then(({ data }) => { this.state.lists.error = false; - this.state.lists.data = data.Buckets.sort(this.sortByName); + this.state.lists.data = data.Buckets.sort(sortByName); }) .catch(() => { this.state.lists.error = true; }) .finally(() => { this.state.lists.requesting = false; + this.$breadcrumb.updateBucketPath(this.state.lists.data.length); }); } @@ -140,7 +127,7 @@ export default class BucketService { createBucket(bucket) { this.$fetch.post('/v1/bucket/create', { bucket }) .then(({ data }) => { - this.state.lists.data = data.Buckets.sort(this.sortByName); + this.state.lists.data = data.Buckets.sort(sortByName); this.$toast.show(`Bucket ${bucket} has created!`); }) .catch(() => { diff --git a/src/components/file/file.controller.js b/src/components/file/file.controller.js new file mode 100644 index 0000000..ac164d4 --- /dev/null +++ b/src/components/file/file.controller.js @@ -0,0 +1,34 @@ +export default class FileController { + /** @ngInject */ + constructor($scope, $stateParams, $file, $bucket, $breadcrumb, $upload) { + Object.assign(this, { + $file, $upload, $bucket, $breadcrumb, + }); + + $scope.$watch( + () => $file.state.lists, + newVal => Object.assign(this, newVal) + , true); + + const paths = $stateParams.path.split('/'); + const [bucket, ...folders] = paths; + + this.$file.setPaths(bucket, folders); + this.$breadcrumb.updateFilePath(paths); + + this.$bucket.getBuckets(); + this.$file.getFiles(); + } + + createFolder($event) { + this.$file.createFolder($event); + } + + upload($event) { + this.$upload.createDialog($event); + } + + refresh() { + this.$file.getFiles(); + } +} diff --git a/src/components/file/file.css b/src/components/file/file.css new file mode 100644 index 0000000..551d8f5 --- /dev/null +++ b/src/components/file/file.css @@ -0,0 +1,24 @@ + +/** + * @author Jamie jamie.h@inwinstack.com + */ + +.checkbox-icon-width { + width: 80px; +} + +.storage-class-width { + width: 140px; +} + +.size-width { + width: 84px; +} + +.time-width { + width: 270px; +} + +.time-title-width { + width: 286px; +} \ No newline at end of file diff --git a/src/components/file/file.html b/src/components/file/file.html new file mode 100644 index 0000000..86eb9cd --- /dev/null +++ b/src/components/file/file.html @@ -0,0 +1,99 @@ + +
+ + + + + + + + + + +
NameStorage ClassSizeLast Modified
+ + + + insert_drive_file +

+ +

+

+ +

+

+ +

+

+ +

+
+
+
+ +
+ +
Loading...
+
+ +
+
This bucket is empty
+
You can do the following actions
+ +
+ + file_upload + Upload File + + + or + + + create_new_folder + Create Folder + +
+
+ +
+
Oops, your connection seems off...
+
Don't worry. You can refresh to try again.
+ + + refresh + +
+
diff --git a/src/components/file/file.js b/src/components/file/file.js new file mode 100644 index 0000000..469bb64 --- /dev/null +++ b/src/components/file/file.js @@ -0,0 +1,29 @@ +import { module } from 'angular'; +import router from 'angular-ui-router'; + +import FileController from './file.controller'; +import FileService from './file.service'; +import FileTemplate from './file.html'; +import UploadService from './upload/upload.servce'; +import './file.css'; + +/** @ngInject */ +const route = $stateProvider => { + $stateProvider.state('file', { + url: '/bucket/*path', + parent: 'root', + controller: FileController, + controllerAs: 'file', + template: FileTemplate, + onEnter: $nav => $nav.setTypeToFile(), + }); +}; + +const File = module('file', [ + router, +]) +.service('$file', FileService) +.service('$upload', UploadService) +.config(route); + +export default File.name; diff --git a/src/components/file/file.service.js b/src/components/file/file.service.js new file mode 100644 index 0000000..54c721e --- /dev/null +++ b/src/components/file/file.service.js @@ -0,0 +1,49 @@ +export default class FileService { + /** @ngInject */ + constructor($mdDialog, $fetch, $bucket) { + Object.assign(this, { + $mdDialog, $fetch, $bucket, + }); + + this.initState(); + } + + initState() { + this.state = { + paths: { + bucket: '', + folders: [], + }, + lists: { + data: [], + requesting: false, + error: false, + }, + }; + } + + setPaths(bucket, folders) { + this.paths = { bucket, folders }; + } + + getFiles() { + const { bucket, folders } = this.paths; + const endpoint = `/v1/file/list/${bucket}?prefix=${folders.join('/')}`; + + this.state.lists.requesting = true; + this.state.lists.data = []; + + this.$fetch + .get(endpoint) + .then(({ data }) => { + this.state.lists.error = false; + this.state.lists.data = data.files || []; + }) + .catch(() => { + this.state.lists.error = true; + }) + .finally(() => { + this.state.lists.requesting = false; + }); + } +} diff --git a/src/components/file/file.spec.js b/src/components/file/file.spec.js new file mode 100644 index 0000000..0d0d51f --- /dev/null +++ b/src/components/file/file.spec.js @@ -0,0 +1,213 @@ +import app from './../../index.js'; +import fileCtrl from './file.controller'; +import fileService from './file.service'; +import bucketService from '../bucket/bucket.service'; +import uploadService from './upload/upload.servce'; + +describe('File Unit Test', function() { + let $rootScope; + let makeService; + let BucService; + let makeUpService; + let makeDeferred; + let makeController; + let $fetch; + let $toast; + let $mdDialog; + let $breadcrumb; + let Upload; + let Config = { API_URL: '0.0.0.0:0000' }; + let $stateParams = { + path: 'BucketName/FolderA/FolderB' + }; + + beforeEach(angular.mock.module('app')); + + beforeEach(inject(($q, _Upload_, _$rootScope_, _$mdDialog_, _$toast_, _$breadcrumb_, _$fetch_) => { + $rootScope = _$rootScope_; + + $toast = _$toast_; + + $fetch = _$fetch_; + + $mdDialog = _$mdDialog_; + + Upload = _Upload_; + + $breadcrumb = _$breadcrumb_; + + makeService = () => { + return new fileService($mdDialog, $fetch, BucService); + }; + + makeUpService = (service) => { + return new uploadService(Config, Upload, $mdDialog, service, $toast); + }; + + makeDeferred = () => { + return $q.defer(); + }; + + BucService = new bucketService($fetch, $toast, $mdDialog, $breadcrumb); + + + makeController = (service, upService) => { + return new fileCtrl($rootScope, $stateParams, service, BucService, $breadcrumb, upService); + }; + })); + describe('when init service', function() { + let service; + + beforeEach(function() { + service = makeService(); + }); + + it('should declare paths', function() { + expect(service.state.paths.bucket).to.eq(''); + expect(service.state.paths.folders).to.be.empty; + }); + it('should declare lists', function() { + expect(service.state.lists.data).to.be.empty; + expect(service.state.lists.requesting).to.eq(false); + expect(service.state.lists.error).to.eq(false); + }); + }); + + describe('when setPaths in service', function() { + let service; + + beforeEach(function() { + service = makeService(); + service.setPaths('BucketName', ["FolderA", "FolderB"]); + }); + it('should put value to service.paths', function() { + expect(service.paths).to.have.property('bucket', 'BucketName'); + expect(service.paths.folders).to.have.property([0], 'FolderA'); + expect(service.paths.folders).to.have.property([1], 'FolderB'); + }); + }); + + describe('when getFiles in service and response success', function() { + let service; + let testMock; + let fetchMock; + let deferred; + let res; + beforeEach(function() { + service = makeService(); + deferred = makeDeferred(); + fetchMock = sinon.mock(service.$fetch); + fetchMock.expects('get').returns(deferred.promise); + res = { + "files": [ + { + "name": "fileName", + "Size": "323844" + } + ] + } + service.paths = { bucket: 'BucketName', folders: ['FolderA', 'FolderB']}; + deferred.resolve({ data:res }); + service.getFiles(); + $rootScope.$digest(); + }); + it('should requesting to be false', function() { + expect(service.state.lists.requesting).to.eq(false); + }); + it('should let data have files information', function() { + expect(service.state.lists.data).to.have.deep.property('[0].name', "fileName"); + expect(service.state.lists.data).to.have.deep.property('[0].Size', "323844") + }); + it('should let error to be false', function() { + expect(service.state.lists.error).to.eq(false); + }) + }); + + describe('when getFiles in service and response fail', function() { + let service; + let testMock; + let fetchMock; + let deferred; + beforeEach(function() { + service = makeService(); + deferred = makeDeferred(); + fetchMock = sinon.mock(service.$fetch); + fetchMock.expects('get').returns(deferred.promise); + service.paths = { bucket: 'BucketName', folders: ['FolderA', 'FolderB']}; + deferred.reject(); + service.getFiles(); + $rootScope.$digest(); + }); + it('should requesting to be false', function() { + expect(service.state.lists.requesting).to.eq(false); + }); + it('should let error to be true', function() { + expect(service.state.lists.error).to.eq(true); + }) + }); + + describe('when init controller', function() { + let service; + let controller; + let mockSetPaths; + let mockGetFiles; + let mockUpdateFP; + let mockGetBucket; + beforeEach(function() { + service = makeService(); + service.getFiles = () => {}; + mockSetPaths = sinon.spy(service, 'setPaths'); + mockUpdateFP = sinon.spy($breadcrumb, 'updateFilePath'); + mockGetBucket = sinon.spy(BucService, 'getBuckets') + mockGetFiles = sinon.spy(service, 'getFiles'); + controller = makeController(service); + }); + it('should invoke setPaths in fileService and call by PATH', function() { + expect(mockSetPaths).to.have.been.calledWith('BucketName', ["FolderA", "FolderB"]); + }); + it('should invoke $breadcrumb.updateFilePath by folders', function() { + expect(mockUpdateFP).to.have.been.calledWith($stateParams.path.split('/')); + }); + it('should invoke getbuckets in bucketService', function() { + expect(mockGetBucket.called).to.eq(true); + }); + it('should invoke getFiles in fileService', function() { + expect(mockGetFiles.called).to.eq(true) + }); + }); + describe('when refresh in controller', function() { + let service; + let controller; + let mockGetFiles; + let upService; + beforeEach(function() { + service = makeService(); + service.getFiles = () => {}; + upService = makeUpService(service); + controller = makeController(service); + mockGetFiles = sinon.spy(service, 'getFiles'); + }); + it('should invoke getFiles in service', function() { + expect(mockGetFiles.called).to.eq(false); + controller.refresh(); + expect(mockGetFiles.called).to.eq(true); + }); + }); + describe('when upload in controller', function() { + let service; + let controller; + let mockCreateDialog; + let upService; + beforeEach(function() { + service = makeService(); + upService = makeUpService(service); + controller = makeController(service, upService); + mockCreateDialog = sinon.spy(upService, 'createDialog'); + }); + it('should invoke createDialog in service', function() { + expect(mockCreateDialog.called).to.eq(false); + controller.upload(); + expect(mockCreateDialog.called).to.eq(true); + }); + }); +}); \ No newline at end of file diff --git a/src/components/file/upload/upload.controller.js b/src/components/file/upload/upload.controller.js new file mode 100644 index 0000000..a9f246a --- /dev/null +++ b/src/components/file/upload/upload.controller.js @@ -0,0 +1,29 @@ +export default class FileUploadController { + /** @ngInject */ + constructor($file, $upload, $scope) { + Object.assign(this, { + $file, $upload, $scope, + }); + + $scope.$watch( + () => $upload.state, + newVal => Object.assign(this, newVal) + , true); + } + + upload() { + this.$upload.upload(); + } + + select(files) { + this.$upload.select(files); + } + + delete(name) { + this.$upload.delete(name); + } + + cancel() { + this.$upload.closeDialog(); + } +} diff --git a/src/components/file/upload/upload.html b/src/components/file/upload/upload.html new file mode 100644 index 0000000..5b60208 --- /dev/null +++ b/src/components/file/upload/upload.html @@ -0,0 +1,85 @@ + +
+ +
+

Upload Files

+ + + + + clear + +
+
+ +
+
+

+ To upload files to S3 Portal, click Add Files. To remove files already selected, click the ✖ to the far right of the file name. +

+
+ + + +
+

Add Files

+ add +
+
+ + + + + photo + insert_drive_file + +

+

+ + clear +
+ + + +
+
+
+ + + + Cancel + + + + Upload + + + +
+
diff --git a/src/components/file/upload/upload.servce.js b/src/components/file/upload/upload.servce.js new file mode 100644 index 0000000..f4e6ace --- /dev/null +++ b/src/components/file/upload/upload.servce.js @@ -0,0 +1,92 @@ +import { element } from 'angular'; +import totalSize from '../../../utils/totalSize'; +import FileUploadController from './upload.controller'; +import FileUploadTemplate from './upload.html'; + +export default class FileUploadService { + /** @ngInject */ + constructor(Config, Upload, $mdDialog, $file, $transfer) { + Object.assign(this, { + Config, Upload, $mdDialog, $file, $transfer, + }); + + this.initState(); + } + + initState() { + this.state = { + files: [], + size: 0, + }; + } + + select(selectedFiles) { + const additionalFiles = selectedFiles.filter(selectedFile => + this.state.files.every(({ detail }) => detail.name !== selectedFile.name) + ).map(detail => ({ + id: Symbol('unique id'), detail, + })); + + const files = [...this.state.files, ...additionalFiles]; + const size = totalSize(files); + + this.state = { files, size }; + } + + delete(id) { + const files = this.state.files.filter(file => file.id !== id); + const size = totalSize(files); + + this.state = { files, size }; + } + + upload() { + const { bucket, folders } = this.$file.paths; + const prefix = folders.length ? '' : `${folders.join('/')}/`; + const url = `${this.Config.API_URL}/v1/file/create`; + + this.state.uploading = true; + this.$transfer.put(this.state.files.map(({ + id, detail, + }) => ({ + id, + bucket, + name: detail.name, + type: 'UPLOAD', + status: 'UPLOADING', + upload: this.uploadFile(id, { + bucket, prefix, file: detail, + }, url), + }))); + + this.closeDialog(); + } + + uploadFile(id, data, url) { + const upload = this.Upload.upload({ url, data }); + + upload.then( + res => this.$transfer.handleSuccess(id, res), + err => this.$transfer.handleFailure(id, err), + evt => this.$transfer.handleEvent(id, evt) + ); + + return upload; + } + + createDialog($event) { + this.$mdDialog.show({ + controller: FileUploadController, + controllerAs: 'upload', + template: FileUploadTemplate, + parent: element(document.body), + targetEvent: $event, + clickOutsideToClose: true, + }); + } + + closeDialog() { + this.$mdDialog.cancel(); + this.initState(); + } +} diff --git a/src/components/file/upload/upload.spec.js b/src/components/file/upload/upload.spec.js new file mode 100644 index 0000000..c89e3ff --- /dev/null +++ b/src/components/file/upload/upload.spec.js @@ -0,0 +1,346 @@ +import app from '../../../index.js'; +import upCtrl from './upload.controller'; +import fileService from '../file.service'; +import uploadService from './upload.servce'; +import transService from '../../layout/transfer/transfer.service'; + +describe('Upload Unit Test', function() { + let $rootScope; + let makeFileService; + let BucService; + let makeUpService; + let makeTransService; + let makeDeferred; + let makeController; + let $fetch; + let $toast; + let $mdDialog; + let $breadcrumb; + let Upload; + let Config = { API_URL: '0.0.0.0:0000' }; + let $stateParams = { + path: 'BucketName/FolderA/FolderB' + }; + + beforeEach(angular.mock.module('app')); + + beforeEach(inject(($q, _Upload_, _$rootScope_, _$mdDialog_, _$toast_, _$breadcrumb_, _$fetch_) => { + $rootScope = _$rootScope_; + + $fetch = _$fetch_; + + $toast = _$toast_; + + $mdDialog = _$mdDialog_; + + Upload = _Upload_; + + BucService = {}; + + makeFileService = () => { + return new fileService($mdDialog, $fetch, BucService); + }; + + makeUpService = (fileService, transService) => { + return new uploadService(Config, Upload, $mdDialog, fileService, transService); + }; + + makeDeferred = () => { + return $q.defer(); + }; + + makeController = (fileService, upService) => { + return new upCtrl(fileService, upService, $rootScope); + }; + + makeTransService = (fileService) => { + return new transService($toast, fileService); + }; + })); + describe('when upload in controller', function() { + let service; + let controller; + let mockUpload; + beforeEach(function() { + service = makeUpService(makeFileService()); + controller = makeController(makeFileService(), service); + service.upload = () => {}; + mockUpload = sinon.spy(service, 'upload'); + }); + + it('should invoke upload in service', function() { + expect(mockUpload.called).to.eq(false); + controller.upload(); + expect(mockUpload.called).to.eq(true); + }); + }); + describe('when select in controller', function() { + let service; + let controller; + let mockSelect; + beforeEach(function() { + service = makeUpService(makeFileService()); + controller = makeController(makeFileService(), service); + service.select = () => {}; + mockSelect = sinon.spy(service, 'select'); + }); + + it('should invoke upload in service', function() { + expect(mockSelect.called).to.eq(false); + controller.select(123); + expect(mockSelect).to.have.been.calledWith(123); + }); + }); + describe('when delete in controller', function() { + let service; + let controller; + let mockDelete; + beforeEach(function() { + service = makeUpService(makeFileService()); + controller = makeController(makeFileService(), service); + service.delete = () => {}; + mockDelete = sinon.spy(service, 'delete'); + }); + + it('should invoke upload in service', function() { + expect(mockDelete.called).to.eq(false); + controller.delete(321); + expect(mockDelete).to.have.been.calledWith(321); + }); + }); + describe('when cancel in controller', function() { + let service; + let controller; + let mockCloseDialog; + beforeEach(function() { + service = makeUpService(makeFileService()); + controller = makeController(makeFileService(), service); + service.closeDialog = () => {}; + mockCloseDialog = sinon.spy(service, 'closeDialog'); + }); + + it('should invoke upload in service', function() { + expect(mockCloseDialog.called).to.eq(false); + controller.cancel(); + expect(mockCloseDialog.called).to.eq(true); + }); + }); + describe('when init service', function() { + let service; + beforeEach(function() { + service = makeUpService(makeFileService()); + }); + it('should declare service.state.files', function() { + expect(service.state.files).to.be.empty; + }); + it('should declare service.state.size', function() { + expect(service.state.size).to.eq(0); + }); + }); + describe('when select non-repeat files in service', function() { + let service; + let mockTotal; + let file = [ + { + 'name': 'fileName', + 'size': 555 + }, + { + 'name': 'fileName2', + 'size': 5858 + } + ] + beforeEach(function() { + service = makeUpService(makeFileService()); + service.select(file); + $rootScope.$digest(); + }); + it('should declare service.state.files', function() { + expect(service.state.files[0].id).to.be.a('symbol'); + expect(service.state.files[0].detail).to.have.property('name', 'fileName'); + expect(service.state.files[0].detail).to.have.property('size', 555); + expect(service.state.files[1].id).to.be.a('symbol'); + expect(service.state.files[1].detail).to.have.property('name', 'fileName2'); + expect(service.state.files[1].detail).to.have.property('size', 5858); + }); + it('should total size', function() { + expect(service.state.size).to.eq(file.reduce((p, c) => p+c.size,0)); + }); + }); + describe('when select same name files', function() { + let service; + let mockTotal; + let file = [ + { + 'name': 'fileName', + 'size': 555 + }, + { + 'name': 'fileName2', + 'size': 5858 + } + ] + beforeEach(function() { + service = makeUpService(makeFileService()); + service.state.files = [ { + id: Symbol('unique id'), + detail: { + name: 'fileName2', + size: 808 + } + }]; + service.select(file); + $rootScope.$digest(); + }); + it('should not insert repeat files', function() { + expect(service.state.files[0].id).to.be.a('symbol'); + expect(service.state.files[0].detail).to.have.property('name', 'fileName2'); + expect(service.state.files[0].detail).to.have.property('size', 808); + expect(service.state.files[1].id).to.be.a('symbol'); + expect(service.state.files[1].detail).to.have.property('name', 'fileName'); + expect(service.state.files[1].detail).to.have.property('size', 555); + }); + it('should total size', function() { + expect(service.state.size).to.eq(service.state.files.reduce((p, c) => p+c.detail.size,0)); + }); + }); + describe('when delete in service', function() { + let service; + let mockTotal; + let file = [ + { + id: Symbol('unique id'), + detail: { + name: 'fileName1', + size: 8585 + } + }, + { + id: Symbol('unique id'), + detail: { + name: 'fileName2', + size: 808 + } + } + ]; + beforeEach(function() { + service = makeUpService(makeFileService()); + service.state.files = file; + }); + it('should delete files', function() { + expect(service.state.files[0].id).to.be.a('symbol'); + expect(service.state.files[0].detail).to.have.property('name', 'fileName1'); + expect(service.state.files[0].detail).to.have.property('size', 8585); + expect(service.state.files[1].id).to.be.a('symbol'); + expect(service.state.files[1].detail).to.have.property('name', 'fileName2'); + expect(service.state.files[1].detail).to.have.property('size', 808); + service.delete(service.state.files[0].id); + expect(service.state.files[0].id).to.be.a('symbol'); + expect(service.state.files[0].detail).to.have.property('name', 'fileName2'); + expect(service.state.files[0].detail).to.have.property('size', 808); + }); + it('should total size', function() { + service.delete(service.state.files[0].id); + expect(service.state.size).to.eq(service.state.files.reduce((p, c) => p+c.detail.size,0)); + }); + }); + describe('when upload in service', function() { + let service; + let fileService; + let transService; + let mockPut; + let mockCloseDialog; + let mockUploadFile; + beforeEach(function() { + fileService = makeFileService(); + transService = makeTransService(fileService); + fileService.paths = { bucket: 'BucketName', folders: ['FolderA', 'FolderB']}; + service = makeUpService(fileService, transService); + service.state.files = [ + { + id: Symbol('unique id'), + detail: { + name: 'FileName', + size: 888 + } + } + ]; + service.uploadFile = () => { return 'aPromise' }; + service.closeDialog = () => {}; + mockCloseDialog = sinon.spy(service, 'closeDialog'); + mockUploadFile = sinon.spy(service, 'uploadFile'); + mockPut = sinon.spy(transService, 'put'); + service.upload(); + $rootScope.$digest(); + }); + it('should let uploading to be true', function() { + expect(service.state.uploading).to.eq(true); + }); + it('should invoke $transfer.put and call by right way', function() { + const { bucket, folders } = fileService.paths; + const called = [{ + id: service.state.files[0].id, + bucket: bucket, + name: service.state.files[0].detail.name, + type: 'UPLOAD', + status: 'UPLOADING', + upload: 'aPromise' + }]; + expect(mockPut).to.have.been.calledWith(called); + }); + it('should invoke uploadFile and call by id, data and url', function() { + const { bucket, folders } = fileService.paths; + const prefix = folders.length ? '' : `${folders.join('/')}/`; + const called = { + id: service.state.files[0].id, + data: { + bucket: bucket, + prefix: prefix, + file: service.state.files[0].detail + }, + url:`${Config.API_URL}/v1/file/create` + } + expect(mockUploadFile).to.have.been.calledWith(called.id, called.data, called.url); + }); + it('should invoke closeDialog', function() { + expect(mockCloseDialog.called).to.eq(true); + }); + }); + describe('when createDialog in service', function() { + let service; + let fileService; + let transService; + let mockCreateDialog; + beforeEach(function() { + fileService = makeFileService(); + transService = makeTransService(fileService); + service = makeUpService(fileService, transService); + mockCreateDialog = sinon.spy($mdDialog, 'show'); + service.createDialog(); + }); + it('should invoke mdDialog.show', function() { + expect(mockCreateDialog.called).to.eq(true); + }); + }); + describe('when closeDialog in service', function() { + let service; + let fileService; + let transService; + let mockCancelDialog; + let mockInitState; + beforeEach(function() { + fileService = makeFileService(); + transService = makeTransService(fileService); + service = makeUpService(fileService, transService); + mockInitState = sinon.spy(service, 'initState'); + mockCancelDialog = sinon.spy($mdDialog, 'cancel'); + service.closeDialog(); + }); + it('should invoke mdDialog.cancel', function() { + expect(mockCancelDialog.called).to.eq(true); + }); + it('should invoke initState in service', function() { + expect(mockInitState.called).to.eq(true); + }); + }); +}); \ No newline at end of file diff --git a/src/components/index.js b/src/components/index.js index 8c1c994..d209813 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -3,12 +3,14 @@ import Layout from './layout/layout'; import NotFound from './not-found/not-found'; import Auth from './auth/auth'; import Bucket from './bucket/bucket'; +import File from './file/file'; const Components = module('app.components', [ Layout, NotFound, Auth, Bucket, + File, ]); export default Components.name; diff --git a/src/components/layout/action-navbar/action-navbar.controller.js b/src/components/layout/action-navbar/action-navbar.controller.js index 34da8c9..a2e41c1 100644 --- a/src/components/layout/action-navbar/action-navbar.controller.js +++ b/src/components/layout/action-navbar/action-navbar.controller.js @@ -1,14 +1,19 @@ export default class ActionNavbarController { /** @ngInject */ - constructor($scope, $bucket, $nav) { + constructor($scope, $bucket, $nav, $file, $upload, $layout) { Object.assign(this, { - $scope, $bucket, + $scope, $bucket, $file, $upload, $layout, }); this.$scope.$watch( () => $nav.type, newVal => (this.type = newVal) ); + + this.$scope.$watch( + () => $layout.state, + newVal => Object.assign(this, newVal) + ); } /** @@ -28,24 +33,20 @@ export default class ActionNavbarController { // } - upload() { - // - } - delete() { // } - none() { - // + closeSidePanels() { + this.$layout.closeSidePanels(); } - properties() { - // + openProperties() { + this.$layout.openProperties(); } - transfers() { - // + openTransfers() { + this.$layout.openTransfers(); } /** @@ -56,12 +57,16 @@ export default class ActionNavbarController { */ create($event) { if (this.isFile()) { - // create file dialog + this.$upload.createDialog($event); } else { this.$bucket.createDialog($event); } } + createFolder($event) { + // handle the create folder event + } + /** * Refresh the list by `this.type` * @@ -69,7 +74,7 @@ export default class ActionNavbarController { */ refresh() { if (this.isFile()) { - // get the files + this.$file.getFiles(); } else { this.$bucket.getBuckets(); } diff --git a/src/components/layout/action-navbar/action-navbar.html b/src/components/layout/action-navbar/action-navbar.html index 8cbd1cb..7455063 100644 --- a/src/components/layout/action-navbar/action-navbar.html +++ b/src/components/layout/action-navbar/action-navbar.html @@ -2,22 +2,21 @@
- file_upload - Upload + add + file_upload + Create Bucket + Upload - add - create_new_folder - Create Bucket - Create Folder + create_new_folder + Create Folder @@ -50,22 +49,22 @@ - + - Upload + Create Bucket + Upload - + - Create Bucket - Create Folder + Create Folder @@ -95,7 +94,7 @@ Properties @@ -107,14 +106,15 @@ None info_outline Properties @@ -122,7 +122,7 @@ Transfers diff --git a/src/components/layout/breadcrumb/breadcrumb.controller.js b/src/components/layout/breadcrumb/breadcrumb.controller.js new file mode 100644 index 0000000..9eb320a --- /dev/null +++ b/src/components/layout/breadcrumb/breadcrumb.controller.js @@ -0,0 +1,13 @@ +export default class BreadcrumbController { + /** @ngInject */ + constructor($scope, $bucket, $breadcrumb) { + Object.assign(this, { + $scope, $bucket, $breadcrumb, + }); + + this.$scope.$watch( + () => $breadcrumb.paths, + newVal => (this.paths = newVal) + , true); + } +} diff --git a/src/components/layout/breadcrumb/breadcrumb.html b/src/components/layout/breadcrumb/breadcrumb.html new file mode 100644 index 0000000..8f44a1f --- /dev/null +++ b/src/components/layout/breadcrumb/breadcrumb.html @@ -0,0 +1,8 @@ + diff --git a/src/components/layout/breadcrumb/breadcrumb.service.js b/src/components/layout/breadcrumb/breadcrumb.service.js new file mode 100644 index 0000000..259fd4a --- /dev/null +++ b/src/components/layout/breadcrumb/breadcrumb.service.js @@ -0,0 +1,47 @@ +export default class BreadcrumbService { + /** @ngInject */ + constructor() { + this.initPaths(); + } + + /** + * Initial the paths state. + * + * @return {void} + */ + initPaths() { + this.paths = [{ + link: '/bucket', + text: 'All Bucket', + isBucket: true, + len: 0, + }]; + } + + /** + * Update the files length of bucket. + * + * @param {integer} len + * + * @return {void} + */ + updateBucketPath(len) { + this.paths[0].len = len; + } + + /** + * Update paths in breadcrumb bar. + * + * @param {Array} paths + * + * @return {void} + */ + updateFilePath(paths) { + this.initPaths(); + paths.reduce((previous, current) => { + const link = `${previous}/${current}`; + this.paths.push({ link, text: current }); + return link; + }, '/bucket'); + } +} diff --git a/src/components/layout/layout.controller.js b/src/components/layout/layout.controller.js new file mode 100644 index 0000000..854e082 --- /dev/null +++ b/src/components/layout/layout.controller.js @@ -0,0 +1,12 @@ +export default class LayoutController { + /** @ngInject */ + constructor($layout) { + Object.assign(this, { + $layout, + }); + } + + toggleTransfer() { + this.$layout.toggleTransfer(); + } +} diff --git a/src/components/layout/layout.html b/src/components/layout/layout.html index 363d63c..ca51d50 100644 --- a/src/components/layout/layout.html +++ b/src/components/layout/layout.html @@ -3,11 +3,15 @@
- +
- +
diff --git a/src/components/layout/layout.js b/src/components/layout/layout.js index 09bbabd..a5c8c11 100644 --- a/src/components/layout/layout.js +++ b/src/components/layout/layout.js @@ -1,14 +1,23 @@ import { module } from 'angular'; import router from 'angular-ui-router'; +import LayoutController from './layout.controller'; +import LayoutService from './layout.service'; import LayoutTemplate from './layout.html'; import TopNavbarController from './top-navbar/top-navbar.controller'; import TopNavbarTemplate from './top-navbar/top-navbar.html'; +import BreadcrumbController from './breadcrumb/breadcrumb.controller'; +import BreadcrumbTemplate from './breadcrumb/breadcrumb.html'; +import BreadcrumbService from './breadcrumb/breadcrumb.service'; import ActionNavbarController from './action-navbar/action-navbar.controller'; import ActionNavbarTemplate from './action-navbar/action-navbar.html'; import ActionNavbarService from './action-navbar/action-navbar.service'; +import TransferController from './transfer/transfer.controller'; +import TransferTemplate from './transfer/transfer.html'; +import TransferService from './transfer/transfer.service'; import './layout.css'; +import './transfer/transfer.css'; /** @ngInject */ const route = $stateProvider => { @@ -18,6 +27,8 @@ const route = $stateProvider => { views: { '': { template: LayoutTemplate, + controller: LayoutController, + controllerAs: 'layout', }, 'top-navbar@root': { template: TopNavbarTemplate, @@ -29,6 +40,16 @@ const route = $stateProvider => { controller: ActionNavbarController, controllerAs: 'actionNav', }, + 'breadcrumb@root': { + template: BreadcrumbTemplate, + controller: BreadcrumbController, + controllerAs: 'bc', + }, + 'transfer@root': { + template: TransferTemplate, + controller: TransferController, + controllerAs: 'transfer', + }, }, }); }; @@ -36,7 +57,10 @@ const route = $stateProvider => { const Layout = module('layout', [ router, ]) +.service('$breadcrumb', BreadcrumbService) .service('$nav', ActionNavbarService) +.service('$layout', LayoutService) +.service('$transfer', TransferService) .config(route); export default Layout.name; diff --git a/src/components/layout/layout.service.js b/src/components/layout/layout.service.js new file mode 100644 index 0000000..7e43a64 --- /dev/null +++ b/src/components/layout/layout.service.js @@ -0,0 +1,33 @@ +export default class LayoutService { + constructor() { + this.initState(); + } + + initState() { + this.state = { + transfers: false, + properties: false, + }; + } + + openProperties() { + this.state = { + transfers: false, + properties: true, + }; + } + + openTransfers() { + this.state = { + transfers: true, + properties: false, + }; + } + + closeSidePanels() { + this.state = { + transfers: false, + properties: false, + }; + } +} diff --git a/src/components/layout/top-navbar/top-navbar.controller.js b/src/components/layout/top-navbar/top-navbar.controller.js index 80be162..dd8e829 100644 --- a/src/components/layout/top-navbar/top-navbar.controller.js +++ b/src/components/layout/top-navbar/top-navbar.controller.js @@ -1,8 +1,8 @@ export default class TopNavbarController { /** @ngInject */ - constructor($translate, $auth, $state, $toast, $mdDialog, AuthService) { + constructor($scope, $translate, $auth, $state, $toast, $mdDialog, $transfer, AuthService) { Object.assign(this, { - $translate, $auth, $state, $toast, $mdDialog, AuthService, + $scope, $translate, $auth, $state, $toast, $mdDialog, $transfer, AuthService, }); this.languages = [ @@ -32,7 +32,11 @@ export default class TopNavbarController { * @return {void} */ signOut($event) { - this.showConfirmMessage($event).then(this.executedSignOut); + if (this.$transfer.isProcessing()) { + this.showConfirmMessage($event).then(this.executedSignOut); + } else { + this.executedSignOut(); + } } /** @@ -61,6 +65,7 @@ or uploads and leaving now will cancel them.Still leaving?`) */ executedSignOut = () => this.AuthService.signOut() .then(() => { + this.$transfer.abort(); this.$auth.logout(); this.$state.go('auth.signin'); this.$toast.show('Sign Out Success!'); diff --git a/src/components/layout/transfer/transfer.controller.js b/src/components/layout/transfer/transfer.controller.js new file mode 100644 index 0000000..f6be220 --- /dev/null +++ b/src/components/layout/transfer/transfer.controller.js @@ -0,0 +1,52 @@ +export default class TransferController { + /** @ngInject */ + constructor($scope, $layout, $transfer) { + Object.assign(this, { + $layout, $transfer, + }); + + $scope.$watch( + () => $transfer.state, + newVal => Object.assign(this, newVal) + , true); + } + + toggleAutoClear() { + this.$transfer.toggleAutoClear(); + } + + close() { + this.$layout.closeSidePanels(); + } + + md2line(t) { + const status = ['FAILED', 'DELETED', 'PAUSED', 'COMPLETED']; + return status.indexOf(t.status) >= 0; + } + + md3line(t) { + const status = ['UPLOADING', 'RESUMING']; + return status.indexOf(t.status) >= 0; + } + + isUpload(t) { + return t.type === 'UPLOAD'; + } + + isDelete(t) { + return t.type === 'DELETE'; + } + + isUploading(t) { + return t.status === 'UPLOADING'; + } + + isCompleted(t) { + return t.status === 'COMPLETED'; + } + + showInfo(t) { + const status = ['FAILED', 'PAUSED']; + return status.indexOf(t.status) < 0; + } +} diff --git a/src/components/layout/transfer/transfer.css b/src/components/layout/transfer/transfer.css new file mode 100644 index 0000000..70da214 --- /dev/null +++ b/src/components/layout/transfer/transfer.css @@ -0,0 +1,20 @@ + +/** + * @author Jamie jamie.h@inwinstack.com + */ + +.transfer-list md-list-item { + border-bottom: 1px solid #eee; +} + +.transfer-list md-list-item:last-child { + border-bottom: none; +} + +.transfer-loaded { + padding-right: 20px; +} + +.transfer-rate { + margin: 10px 20px 10px 0; +} \ No newline at end of file diff --git a/src/components/layout/transfer/transfer.html b/src/components/layout/transfer/transfer.html new file mode 100644 index 0000000..fcc6b48 --- /dev/null +++ b/src/components/layout/transfer/transfer.html @@ -0,0 +1,98 @@ + +
+ +
+

+ Transfers +

+ + + + + clear + +
+
+ + + + + +

Automatically clear finished transfers

+
+ + + file_upload + delete + +
+

+ +
+ +
+ +

+ + + + + + / + + + + + + % + +

+
+ + check_circle + + +
+ +
+
+
+
+
+
diff --git a/src/components/layout/transfer/transfer.service.js b/src/components/layout/transfer/transfer.service.js new file mode 100644 index 0000000..a8128ae --- /dev/null +++ b/src/components/layout/transfer/transfer.service.js @@ -0,0 +1,92 @@ +export default class TransferService { + /** @ngInject */ + constructor($toast, $file) { + Object.assign(this, { + $toast, $file, + }); + this.initState(); + } + + initState() { + this.state = { + autoClear: false, + processing: false, + transfers: [], + }; + } + + isProcessing() { + return this.state.processing; + } + + toggleAutoClear() { + this.state.autoClear = ! this.state.autoClear; + } + + put(transfers) { + this.state.processing = true; + this.state.transfers = [ + ...this.state.transfers, + ...transfers, + ]; + } + + abort() { + this.state.transfers.forEach(transfer => { + if (transfer.status === 'UPLOADING') { + transfer.upload.abort(); + } + }); + this.state.transfers = []; + } + + remove(id) { + this.state.transfers = this.state.transfers.filter( + (transfer, index) => index !== id + ); + } + + findTransferIndex(id) { + return this.state.transfers.findIndex(transfer => transfer.id === id); + } + + handleEvent(id, { loaded, total }) { + const i = this.findTransferIndex(id); + const precentage = (loaded / total * 100).toFixed(2); + this.state.transfers[i].process = { + loaded, total, precentage, + }; + } + + handleSuccess(id) { + const i = this.findTransferIndex(id); + this.state.transfers[i].status = 'COMPLETED'; + this.$toast.show(`${this.state.transfers[i].name} is uploaded successfully!`); + + if (this.state.autoClear) { + this.remove(i); + } + + this.updateProcessStatus(); + this.$file.getFiles(); + } + + handleFailure(id, { statusText }) { + const i = this.findTransferIndex(id); + this.state.transfers[i] = { + ...this.state.transfers[i], + status: 'FAILED', + message: statusText, + }; + this.$toast.show( + `${this.state.transfers[i].name} is uploaded failure! Error message: ${statusText}` + ); + this.updateProcessStatus(); + } + + updateProcessStatus() { + this.state.process = this.state.transfers.every( + transfer => transfer.status !== 'UPLOADING' && transfer.status !== 'RESUMING' + ); + } +} diff --git a/src/config/breadcrumb.config.js b/src/config/breadcrumb.config.js deleted file mode 100644 index c4c08e7..0000000 --- a/src/config/breadcrumb.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import template from './breadcrumb.html'; - -/** @ngInject */ -export default $breadcrumbProvider => $breadcrumbProvider.setOptions({ template }); diff --git a/src/config/breadcrumb.html b/src/config/breadcrumb.html deleted file mode 100644 index fc71562..0000000 --- a/src/config/breadcrumb.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/config/http.config.js b/src/config/http.config.js index 7270500..25869aa 100644 --- a/src/config/http.config.js +++ b/src/config/http.config.js @@ -1,7 +1,8 @@ const TokenInterceptor = ($q, $injector) => ({ responseError(rejection) { const { data } = rejection; - if (data.error && data.error === 'token_not_provided') { + if (data.error && data.error === 'token_not_provided' || data.error === 'token_invalid') { + $injector.get('$auth').logout(); $injector.get('$state').go('auth.signin'); $injector.get('$toast').show('Your token has expired, please sign in again!'); } diff --git a/src/config/index.js b/src/config/index.js index 87b1861..3e4cd5f 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -5,7 +5,6 @@ import satellizer from './satellizer.config'; import material from './material.config'; import authenticateGuard from './AuthenticateGuard'; import http from './http.config'; -import breadcrumb from './breadcrumb.config'; const Config = module('app.config', []) .config(router) @@ -13,7 +12,6 @@ const Config = module('app.config', []) .config(satellizer) .config(material) .config(http) - .config(breadcrumb) .constant('Config', { API_URL: `${process.env.SERVER_HOST}/api`, }) diff --git a/src/config/material.config.js b/src/config/material.config.js index 8916f5e..81663d3 100644 --- a/src/config/material.config.js +++ b/src/config/material.config.js @@ -4,5 +4,5 @@ export default ($mdThemingProvider) => { .theme('default') .primaryPalette('blue') .warnPalette('orange') - .accentPalette('grey'); + .accentPalette('indigo'); }; diff --git a/src/filters/filesize.js b/src/filters/filesize.js new file mode 100644 index 0000000..070dfe9 --- /dev/null +++ b/src/filters/filesize.js @@ -0,0 +1,30 @@ +const units = [ + 'bytes', + 'KB', + 'MB', + 'GB', + 'TB', + 'PB', +]; + +/** + * Format file size. + * + * @return {String} + */ +export default () => bytes => { + if (isNaN(parseFloat(bytes)) || ! isFinite(bytes)) { + return '?'; + } + + let unit = 0; + + while (bytes >= 1024) { + bytes /= 1024; + unit ++; + } + + const result = (unit === 0) ? +bytes : bytes.toFixed(2); + + return `${result} ${units[unit]}`; +}; diff --git a/src/filters/index.js b/src/filters/index.js new file mode 100644 index 0000000..5e45983 --- /dev/null +++ b/src/filters/index.js @@ -0,0 +1,7 @@ +import { module } from 'angular'; +import filesize from './filesize'; + +const Filters = module('app.Filters', []) + .filter('filesize', filesize); + +export default Filters.name; diff --git a/src/index.js b/src/index.js index ccbe9dd..14364b5 100644 --- a/src/index.js +++ b/src/index.js @@ -4,14 +4,16 @@ import './index.css'; import './templates'; import Vendor from './vendor'; import Config from './config'; -import Utils from './utils'; +import Services from './services'; import Directives from './directives'; +import Filters from './filters'; import Components from './components'; module('app', [ Vendor, Config, - Utils, + Services, Directives, + Filters, Components, ]); diff --git a/src/utils/fetch/fetch.js b/src/services/fetch/fetch.js similarity index 100% rename from src/utils/fetch/fetch.js rename to src/services/fetch/fetch.js diff --git a/src/utils/fetch/fetch.service.js b/src/services/fetch/fetch.service.js similarity index 100% rename from src/utils/fetch/fetch.service.js rename to src/services/fetch/fetch.service.js diff --git a/src/utils/index.js b/src/services/index.js similarity index 64% rename from src/utils/index.js rename to src/services/index.js index 623084f..7297e7b 100644 --- a/src/utils/index.js +++ b/src/services/index.js @@ -2,9 +2,9 @@ import { module } from 'angular'; import Toast from './toast/toast'; import Fetch from './fetch/fetch'; -const Utils = module('app.utils', [ +const Services = module('app.services', [ Toast, Fetch, ]); -export default Utils.name; +export default Services.name; diff --git a/src/utils/toast/toast.js b/src/services/toast/toast.js similarity index 100% rename from src/utils/toast/toast.js rename to src/services/toast/toast.js diff --git a/src/utils/toast/toast.service.js b/src/services/toast/toast.service.js similarity index 100% rename from src/utils/toast/toast.service.js rename to src/services/toast/toast.service.js diff --git a/src/styles/base.css b/src/styles/base.css index 8caa00f..ae5f323 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -41,3 +41,35 @@ .text-center { text-align: center; } + +.break-word { + word-break: break-word; +} + +/* display */ + +.inline{ + display: inline; +} + +.block{ + display: block; +} + +.inline-block{ + display: inline-block; +} + +/* align */ + +.valign-bottom{ + vertical-align: bottom; +} + +.valign-middle{ + vertical-align: middle; +} + +.valign-top{ + vertical-align: top; +} diff --git a/src/styles/dialog.css b/src/styles/dialog.css index 1ae5712..dfdca9c 100644 --- a/src/styles/dialog.css +++ b/src/styles/dialog.css @@ -20,3 +20,18 @@ md-dialog.input-dialog { .dialog-description { margin-bottom: 30px; } + +.dialog-footer { + margin-left: 20px; + margin-top: 25px; + text-align: center; + width: 100%; +} + +.dialog-footer span { + padding-right: 10px; +} + +.list-dialog md-list-item p { + margin-right: 40px; +} \ No newline at end of file diff --git a/src/styles/s3.css b/src/styles/s3.css index 1351233..619ba5f 100644 --- a/src/styles/s3.css +++ b/src/styles/s3.css @@ -127,3 +127,40 @@ md-input-container md-progress-circular { .load-fail-state { margin-top: 10%; } + +/* list item --------------------------------------------- */ +md-list-item.checked { + background: #E8EAF6; +} + +md-list-item > .md-list-item-inner > p { + padding: 0 8px; +} + +md-list-item > p.flex-none, +md-list-item > .md-list-item-inner > p.flex-none, +md-list-item .md-list-item-inner > p.flex-none, +md-list-item .md-list-item-inner > .md-list-item-inner > p.flex-none { + flex: 0 0 auto; + -ms-flex: 0 0 auto; +} + +md-list-item > p.flex-grow, +md-list-item > .md-list-item-inner > p.flex-grow, +md-list-item .md-list-item-inner > p.flex-grow, +md-list-item .md-list-item-inner > .md-list-item-inner > p.flex-grow { + flex: 1 1 100%; + -ms-flex: 1 1 100%; +} + +md-list.md-default-theme md-list-item.md-2-line .md-list-item-text p.text-warn, +md-list md-list-item.md-2-line .md-list-item-text p.text-warn, +md-list.md-default-theme md-list-item.md-3-line .md-list-item-text p.text-warn, +md-list md-list-item.md-3-line .md-list-item-text p.text-warn { + color: #FF6D00; +} + +/* fix flexbox type layout issues in IE10 */ +span.flex { + display: block; +} diff --git a/src/styles/table.css b/src/styles/table.css index 7f43265..5e84570 100644 --- a/src/styles/table.css +++ b/src/styles/table.css @@ -27,7 +27,7 @@ th { .table > tfoot > tr > td { padding: 8px; line-height: 1.42857143; - vertical-align: top; + vertical-align: middle; border-top: 1px solid #dddddd; } diff --git a/src/translations/EN.js b/src/translations/EN.js index e0bf219..836bfda 100644 --- a/src/translations/EN.js +++ b/src/translations/EN.js @@ -2,4 +2,16 @@ export default { SETTINGS: { SIGN_OUT: 'Sign Out', }, + TRANSFER: { + TITLE: { + UPLOAD: 'Upload {{ name }} to {{ bucket }}', + DELETE: 'Delete {{ name }} from {{ bucket }}', + }, + STATUS: { + UPLOADING: 'Uploading', + COMPLETED: 'Completed', + DELETE: 'Deleted', + RESUMING: 'Resuming', + }, + }, }; diff --git a/src/utils/sort.js b/src/utils/sort.js new file mode 100644 index 0000000..20a07d5 --- /dev/null +++ b/src/utils/sort.js @@ -0,0 +1,18 @@ +import natural from 'javascript-natural-sort'; + +/** + * Return a function that will sort by given key. + * + * @param {String} x + * @param {String} y + * + * @return {Function} + */ +const sortKey = key => (x, y) => natural(x[key], y[key]); + +/** + * Natural sort by Name. + * + * @return {Function} + */ +export const sortByName = sortKey('Name'); diff --git a/src/utils/totalSize.js b/src/utils/totalSize.js new file mode 100644 index 0000000..61d9b6e --- /dev/null +++ b/src/utils/totalSize.js @@ -0,0 +1,9 @@ +/** + * Calculate the total size of files. + * + * @type {Array} + */ +export default files => files + .reduce((previous, current) => + previous + current.detail.size, 0 + ); diff --git a/src/vendor/index.js b/src/vendor/index.js index 7c92515..eb26892 100644 --- a/src/vendor/index.js +++ b/src/vendor/index.js @@ -3,8 +3,8 @@ import router from 'angular-ui-router'; import material from 'angular-material'; import translate from 'angular-translate'; import validationMatch from 'angular-validation-match'; +import fileUpload from 'ng-file-upload'; import satellizer from 'satellizer'; -import 'angular-breadcrumb'; const Vendor = module('app.vendor', [ router, @@ -12,7 +12,7 @@ const Vendor = module('app.vendor', [ translate, validationMatch, satellizer, - 'ncy-angular-breadcrumb', + fileUpload, ]); export default Vendor.name;