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..e30e595 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,19 @@ export default class BucketController { newVal => Object.assign(this, newVal) , true); + $breadcrumb.initPaths(); this.$bucket.getBuckets(); } createBucket($event) { this.$bucket.createDialog($event); } + + clickBucket(path) { + this.$state.go('file', { path }); + } + + selectBucket(name) { + this.$bucket.selectBucket(name); + } } diff --git a/src/components/bucket/bucket.html b/src/components/bucket/bucket.html index fd50d39..9f142bb 100644 --- a/src/components/bucket/bucket.html +++ b/src/components/bucket/bucket.html @@ -6,24 +6,30 @@ - + - - - - - - -
Name
- - info_outline - - - -
+ + + + +

+ +

+
+
refresh
- + \ No newline at end of file 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..b1cc6d1 100644 --- a/src/components/bucket/bucket.service.js +++ b/src/components/bucket/bucket.service.js @@ -1,13 +1,15 @@ 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'; +import BucketDeleteController from './delete/delete.controller'; +import BucketDeleteTemplate from './delete/delete.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(); @@ -30,6 +32,9 @@ export default class BucketService { checked: false, duplicated: false, }, + delete: { + name: null, + } }; } @@ -63,6 +68,17 @@ export default class BucketService { }); } + deleteDialog($event) { + this.$mdDialog.show({ + controller: BucketDeleteController, + controllerAs: 'delete', + template: BucketDeleteTemplate, + parent: element(document.body), + targetEvent: $event, + clickOutsideToClose: true, + }); + } + /** * Close the dialog. * @@ -73,18 +89,31 @@ 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; + selectBucket(name) { + const { data } = this.state.lists; + const index = data.findIndex(bucket => bucket.Name === name); + this.state.lists.data = data.map((bucket, id) => ({ + ...bucket, + checked: (id === index) ? ! bucket.checked : false, + })); + + this.state.delete.name = this.state.lists.data[index].checked ? data[index].Name : null; + } - return natural(x, y); + deleteBucket() { + const { name } = this.state.delete; + this.$fetch.delete(`/v1/bucket/delete/${name}`) + .then(() => { + this.state.delete.name = null; + this.$toast.show(`Bucket ${name} has been deleted!`); + this.getBuckets(); + }) + .catch(err => { + this.$toast.show(`Bucket ${name} delete failed, please try again!`); + }) + .finally(() => { + this.closeDialog(); + }); } /** @@ -99,13 +128,18 @@ 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); + const buckets = data.Buckets.map(bucket => ({ + ...bucket, + checked: false, + })); + this.state.lists.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 +174,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/bucket/bucket.spec.js b/src/components/bucket/bucket.spec.js new file mode 100644 index 0000000..1b041b3 --- /dev/null +++ b/src/components/bucket/bucket.spec.js @@ -0,0 +1,483 @@ +import bucketCtrl from './bucket.controller'; +import createCtrl from './create/create.controller'; +import bucketServ from './bucket.service'; +import createTem from './create/create.html'; +import app from './../../index.js'; + +describe('bucket testing', function() { + let $rootScope; + let makeService; + let makeDeferred; + let makeController; + let makeCreateController; + let $httpBackend; + let $auth; + let $compile; + let $toast; + let $mdDialog; + let $fetch; + let form; + let $breadcrumb; + let $bucket; + let $state; + + beforeEach(angular.mock.module('app')); + + beforeEach(inject(($q, _$bucket_ ,_$compile_, _$rootScope_, _$auth_, _$toast_, _$mdDialog_, _$breadcrumb_, _$state_, _$fetch_, _$httpBackend_) => { + $rootScope = _$rootScope_; + + $bucket = _$bucket_; + + $compile = _$compile_; + + $fetch = _$fetch_; + + $toast = _$toast_; + + $state = _$state_; + + $mdDialog = _$mdDialog_; + + $auth = _$auth_; + + $breadcrumb = _$breadcrumb_; + + $compile(createTem)($rootScope); + + form = $rootScope.create.form; + + form.bucket.$options.debounce = 0; + + $auth.isAuthenticated = () => true; + + $httpBackend = _$httpBackend_; + + makeService = () => { + return new bucketServ($fetch, $toast, $mdDialog, $breadcrumb); + }; + + makeDeferred = () => { + return $q.defer(); + } + + makeController = (service = $bucket) => { + return new bucketCtrl($rootScope, service, $state, $breadcrumb); + }; + + makeCreateController = (service) => { + return new createCtrl(service, $rootScope); + } + })); + describe('when init service', function() { + it('should let state.lists.requesting to be false', function() { + const service = makeService(); + $rootScope.$digest(); + expect(service.state.lists.requesting).to.eq(false); + }); + it('should let state.lists.error to be false', function() { + const service = makeService(); + $rootScope.$digest(); + expect(service.state.lists.error).to.eq(false); + }); + it('should declare delete.name', () => { + const service = makeService(); + $rootScope.$digest(); + expect(service.state.delete.name).to.be.null; + }); + }); + describe('when resetCheckBucketState in service', function() { + it('should let checking, checked and duplicated be false', function() { + const service = makeService(); + service.getBuckets = () => {}; + service.resetCheckBucketState(); + $rootScope.$digest(); + expect(service.state.create.checking).to.eq(false); + expect(service.state.create.checked).to.eq(false); + expect(service.state.create.duplicated).to.eq(false); + }); + }); + describe('when createDialog', function() { + it('should invoke $mdDialog.show', function() { + const service = makeService(); + const dialog = sinon.spy($mdDialog, 'show'); + service.createDialog(); + $rootScope.$digest(); + expect(dialog.called).to.eq(true); + }); + }); + // describe('when deleteDialog in service', () => { + // it('should invoke $mdDialog.show', () => { + // const service = makeService(); + // const mockDialog = sinon.spy($mdDialog, 'show'); + // service.deleteDialog(); + // $rootScope.$digest(); + // expect(mockDialog.called).to.eq(true); + // }); + // }); + describe('when close Dialog', function() { + it('should invoke $mdDialog.cancel', function() { + const service = makeService(); + const dialog = sinon.spy($mdDialog, 'cancel') + service.closeDialog(); + $rootScope.$digest(); + expect(dialog.called).to.eq(true); + }); + }); + describe('when selectBucket in service', () => { + let service; + beforeEach(() => { + service = makeService(); + service.state.lists.data = [ + {id:'a', Name:'aName'}, { id:'B', Name:'bName'}, + {id:'c', Name:'cName'} + ]; + service.selectBucket('cName') + }); + it('should checked which bucket name called', () => { + expect(service.state.lists.data[2].checked).to.eq(true); + expect(service.state.lists.data[1].checked).to.eq(false); + expect(service.state.lists.data[0].checked).to.eq(false); + }); + }); + describe('when deleteBucket in service and success', () => { + let service; + let deferred; + let mockFetch; + let mockToast; + let mockGetBucket; + let mockClose; + let message; + beforeEach(() => { + service = makeService(); + service.state.lists.data = [ + {id:'a', Name:'aName'}, { id:'B', Name:'bName'}, + {id:'c', Name:'cName'} + ]; + service.state.delete.name = 'aName'; + service.getBuckets = () => {}; + deferred = makeDeferred(); + mockFetch = sinon.mock($fetch); + mockFetch.expects('delete').returns(deferred.promise); + deferred.resolve(); + mockToast = sinon.spy($toast, 'show'); + mockGetBucket = sinon.spy(service, 'getBuckets'); + mockClose = sinon.spy(service, 'closeDialog'); + message = 'Bucket aName has been deleted!'; + service.deleteBucket(); + $rootScope.$digest(); + }); + it('should let state.delete.name to be null', () => { + expect(service.state.delete.name).to.be.null; + }); + it('should invoke $toast.show and call by message', () => { + expect(mockToast).to.have.been.calledWith(message); + }); + it('should invoke getBuckets', () => { + expect(mockGetBucket.called).to.eq(true); + }); + it('should invoke closeDialog', () => { + expect(mockClose.called).to.eq(true); + }); + }); + describe('when deleteBucket in service and fail', () => { + let service; + let mockToast; + let mockFetch; + let deferred; + let mockClose; + let message; + beforeEach(() => { + service = makeService(); + service.state.lists.data = [ + {id:'a', Name:'aName'}, { id:'B', Name:'bName'}, + {id:'c', Name:'cName'} + ]; + service.state.delete.name = 'aName'; + deferred = makeDeferred(); + mockFetch = sinon.mock($fetch); + mockToast = sinon.spy($toast, 'show'); + mockClose = sinon.spy(service, 'closeDialog'); + mockFetch.expects('delete').returns(deferred.promise); + deferred.reject(); + service.deleteBucket(); + message = 'Bucket aName delete failed, please try again!'; + $rootScope.$digest(); + }); + it('should invoke toast.show and call by fail message', () => { + expect(mockToast).to.have.been.calledWith(message); + }); + it('should invoke updateBucketPath', () => { + expect(mockClose.called).to.eq(true); + }); + }); + describe('when getBuckets in service and success', () => { + let service; + let deferred; + let mockUpdate; + let mockFetch; + let res; + beforeEach(() => { + res = {Buckets: + [{ Name: 'b' }, { Name: 'c' }, { Name: 'a'}, + { Name: 'a1'}] + }; + deferred = makeDeferred(); + service = makeService(); + mockFetch = sinon.mock($fetch); + mockUpdate = sinon.spy($breadcrumb, 'updateBucketPath'); + mockFetch.expects('post').returns(deferred.promise); + deferred.resolve({data: res}); + service.getBuckets(); + $rootScope.$digest(); + }); + it('should invoke updateBucketPath and call by length', () => { + expect(mockUpdate).to.have.been.calledWith(res.Buckets.length); + }); + it('should get sorted data', () => { + expect(service.state.lists.data[0].Name).to.eq('a'); + expect(service.state.lists.data[1].Name).to.eq('a1'); + expect(service.state.lists.data[2].Name).to.eq('b'); + expect(service.state.lists.data[3].Name).to.eq('c'); + }); + it('should let requesting to be false', () => { + expect(service.state.lists.requesting).to.eq(false); + }); + }); + describe('when getBuckets in service and fail', () => { + let service; + let deferred; + let mockFetch; + let mockUpdate; + beforeEach(() => { + service = makeService(); + deferred = makeDeferred() + mockFetch = sinon.mock($fetch); + mockFetch.expects('post').returns(deferred.promise); + mockUpdate = sinon.spy($breadcrumb, 'updateBucketPath') + deferred.reject(); + service.getBuckets(); + $rootScope.$digest(); + }); + it('should invoke updateBucketPath and call by length', () => { + expect(mockUpdate).to.have.been.calledWith(0); + }); + it('should let requesting to be false', () => { + expect(service.state.lists.requesting).to.eq(false); + }); + it('should let error to be true', () => { + expect(service.state.lists.error).to.eq(true); + }); + }); + describe('when checkBucket in service and success', function() { + let service; + let deferred; + let mockFetch; + beforeEach(() => { + service = makeService(); + deferred =makeDeferred(); + mockFetch = sinon.mock($fetch) + mockFetch.expects('post').returns(deferred.promise); + deferred.resolve(); + service.checkBucket('BucketName'); + $rootScope.$digest(); + }); + it('should let duplicated to be false', function() { + expect(service.state.create.duplicated).to.eq(false); + }); + it('should let checking to be false', function() { + expect(service.state.create.checking).to.eq(false); + }); + it('should let checked to be true', function() { + expect(service.state.create.checked).to.eq(true); + }); + }); + describe('when checkBucket in service and fail', function() { + let service; + let deferred; + let mockFetch; + beforeEach(() => { + service = makeService(); + deferred =makeDeferred(); + mockFetch = sinon.mock($fetch) + mockFetch.expects('post').returns(deferred.promise); + deferred.reject(); + service.checkBucket('BucketName'); + $rootScope.$digest(); + }); + it('should let duplicated to be true', function() { + expect(service.state.create.duplicated).to.eq(true); + }); + it('should let checking to be false', function() { + expect(service.state.create.checking).to.eq(false); + }); + it('should let checked to be true', function() { + expect(service.state.create.checked).to.eq(true); + }); + }); + describe('when createBucket in service and success', function() { + let service; + let deferred; + let mockFetch; + let mockToast; + let res; + let message; + let mockClose; + beforeEach(() => { + res = {Buckets: + [{ Name: 'b' }, { Name: 'c' }, { Name: 'a'}, + { Name: 'a1'}, {Name: 'BucketName'}] + }; + message = 'Bucket BucketName has created!'; + service = makeService(); + mockClose = sinon.spy(service, 'closeDialog'); + mockToast = sinon.spy($toast, 'show'); + deferred =makeDeferred(); + mockFetch = sinon.mock($fetch) + mockFetch.expects('post').returns(deferred.promise); + deferred.resolve({data: res}); + service.createBucket('BucketName'); + $rootScope.$digest(); + }); + it('should invoke toast.show and called with success message', function() { + expect(mockToast).to.have.been.calledWith(message); + }); + it('should get sorted bucket', function() { + expect(service.state.lists.data[0].Name).to.eq('BucketName'); + expect(service.state.lists.data[1].Name).to.eq('a'); + expect(service.state.lists.data[2].Name).to.eq('a1'); + expect(service.state.lists.data[3].Name).to.eq('b'); + expect(service.state.lists.data[4].Name).to.eq('c'); + }); + it('should invoke closeDialog', function() { + expect(mockClose.called).to.eq(true); + }); + }); + describe('when createBucket reject', function() { + let service; + let deferred; + let mockFetch; + let mockToast; + let res; + let message; + let mockClose; + beforeEach(() => { + res = {Buckets: + [{ Name: 'b' }, { Name: 'c' }, { Name: 'a'}, + { Name: 'a1'}, {Name: 'BucketName'}] + }; + message = 'Bucket create failure, please try again!'; + service = makeService(); + mockClose = sinon.spy(service, 'closeDialog'); + mockToast = sinon.spy($toast, 'show'); + deferred =makeDeferred(); + mockFetch = sinon.mock($fetch) + mockFetch.expects('post').returns(deferred.promise); + deferred.reject(); + service.createBucket('BucketName'); + $rootScope.$digest(); + }); + it('should invoke $toast.show and call with fail message', function() { + expect(mockToast).to.have.been.calledWith(message); + }); + it('should invoke closeDialog', function() { + expect(mockClose.called).to.eq(true); + }); + }); + describe('when init controller', function() { + let service; + let controller; + let mockGet; + let mockBread; + beforeEach(() => { + service = makeService(); + service.getBuckets = () => {}; + mockGet = sinon.spy(service, 'getBuckets'); + mockBread = sinon.spy($breadcrumb, 'initPaths') + controller = makeController(service); + }); + it('should invoke getBuckets in service', function() { + expect(mockGet.called).to.eq(true); + }); + it('should initPaths in breadcrumb service', function() { + expect(mockBread.called).to.eq(true); + }); + }); + describe('when createBucket in bucketCtrl', function() { + it('should invoke service.createDialog', function() { + const service = makeService(); + service.getBuckets = () => {}; + const controller = makeController(service); + const dialog = sinon.spy(service, 'createDialog'); + controller.createBucket(); + $rootScope.$digest(); + expect(dialog.called).to.eq(true); + }); + }); + describe('when clickBucket in bucketCtrl', () => { + let controller; + let mockState; + let path; + beforeEach(() => { + path = { path: 'String' }; + controller = makeController(); + mockState = sinon.spy($state, 'go'); + controller.clickBucket(path); + }); + it('should invoke state.go and call with file and path', () => { + expect(mockState).to.have.been.calledWith('file', {path:path}); + }); + }); + describe('when selectBucket in bucket controller', () => { + let controller; + let mockSelect; + beforeEach(() => { + $bucket.selectBucket = () => {}; + controller = makeController(); + mockSelect = sinon.spy($bucket, 'selectBucket'); + controller.selectBucket('name') + }); + it('should invoke selectBucket in bucket service and called with name', () => { + expect(mockSelect).to.have.been.calledWith('name'); + }); + }); + describe('when fill valid bucket name', function() { + it('should be valid', function() { + form.bucket.$setViewValue('BucketName'); + $rootScope.$digest(); + expect(form.bucket.$viewValue).to.eq('BucketName'); + expect(form.bucket.$valid).to.eq(true); + expect(form.bucket.$invalid).to.eq(false); + }); + }); + describe('when fill a non-valid email', function() { + it('should be invalid', function() { + form.bucket.$setViewValue(''); + $rootScope.$digest(); + expect(form.bucket.$viewValue).to.eq(''); + expect(form.bucket.$valid).to.eq(false); + expect(form.bucket.$invalid).to.eq(true); + }); + }); + describe('when create() in bucketCtrl', function() { + it('should invoke service.createBucket and call with bucket', function() { + const service = makeService(); + service.createBucket = () => {}; + const controller = makeCreateController(service); + controller.bucket = 'BucketName'; + const cBucket = sinon.spy(service, 'createBucket'); + controller.create(); + $rootScope.$digest(); + expect(cBucket).to.have.been.calledWith('BucketName'); + }); + }); + describe('when cancel() in bucketCtrl', function() { + it('should invoke service.closeDialog', function() { + const service = makeService(); + const controller = makeCreateController(service); + const close = sinon.spy(service, 'closeDialog'); + controller.cancel(); + $rootScope.$digest(); + expect(close.called).to.eq(true); + }); + }); +}); \ No newline at end of file diff --git a/src/components/bucket/delete/delete.controller.js b/src/components/bucket/delete/delete.controller.js new file mode 100644 index 0000000..7ae2c43 --- /dev/null +++ b/src/components/bucket/delete/delete.controller.js @@ -0,0 +1,22 @@ +export default class BucketDeleteController { + /** @ngInject */ + constructor($scope, $bucket) { + Object.assign(this, { + $bucket, + }); + + $scope.$watch(() => $bucket.state.delete.name, newVal => this.deleteName = newVal); + } + + check() { + this.checkStatus = this.inputName !== this.deleteName; + } + + deleteBucket() { + this.$bucket.deleteBucket(); + } + + cancel() { + this.$bucket.closeDialog(); + } +} \ No newline at end of file diff --git a/src/components/bucket/delete/delete.html b/src/components/bucket/delete/delete.html new file mode 100644 index 0000000..41a9d77 --- /dev/null +++ b/src/components/bucket/delete/delete.html @@ -0,0 +1,72 @@ + +
+ +
+

Delete Bucket

+ + + + clear + +
+
+ +
+
+
+ error +

+ Amazon S3 buckets are unique. If you delete this bucket, you may lose the bucket name to another AWS user. +

+
+ +

+ Deleting this bucket and its objects (including older versions if applicable) cannot be undone. + Are you sure you want to delete {{ delete.deleteName }}? +

+

Type the name of the bucket to confirm deletion:

+
+ + + + + + + Type the exact name of the bucket you are trying to delete. + + +
+
+
+
+
+
+ + + + Cancel + + + + Delete + + + +
+
\ No newline at end of file diff --git a/src/components/bucket/delete/delete.spec.js b/src/components/bucket/delete/delete.spec.js new file mode 100644 index 0000000..922d2f3 --- /dev/null +++ b/src/components/bucket/delete/delete.spec.js @@ -0,0 +1,56 @@ +import deleteCtrl from './delete.controller'; +import app from '../../../index.js'; + +describe('bucket testing', function() { + let $rootScope; + let makeController; + let $bucket; + + beforeEach(angular.mock.module('app')); + + beforeEach(inject((_$bucket_ , _$rootScope_,) => { + $rootScope = _$rootScope_; + + $bucket = _$bucket_; + + makeController = () => { + return new deleteCtrl($rootScope, $bucket); + }; + })); + describe('when check()', () => { + let controller; + let bool; + it('should declare checkStatus', () => { + controller = makeController(); + controller.inputName = 'Abc'; + controller.deleteName = 'abc'; + $rootScope.$digest(); + controller.check(); + expect(controller.checkStatus).to.eq(true); + }); + }); + describe('when deleteBucket()', () => { + let controller; + let mockDelete; + beforeEach(() => { + controller = makeController(); + mockDelete = sinon.spy($bucket, 'deleteBucket'); + controller.deleteBucket(); + }); + it('should invoke deleteBucket in bucket service', () => { + expect(mockDelete.called).to.eq(true); + }); + }); + describe('when cancel()', () => { + let controller; + let mockClose; + beforeEach(() => { + controller = makeController(); + mockClose = sinon.spy($bucket, 'closeDialog'); + controller.cancel(); + }); + it('should invoke deleteBucket in bucket service', () => { + expect(mockClose.called).to.eq(true); + }); + }); +}); \ No newline at end of file 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/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/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..56fe7c1 100644 --- a/src/components/layout/action-navbar/action-navbar.controller.js +++ b/src/components/layout/action-navbar/action-navbar.controller.js @@ -1,14 +1,24 @@ 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) + ); + + this.$scope.$watch( + () => $bucket.state.delete.name, + newVal => this.disableDeleteButton = ! newVal && ! this.isFile() + ); } /** @@ -28,24 +38,24 @@ export default class ActionNavbarController { // } - upload() { - // - } - delete() { - // + if (this.isFile()) { + // handle delete file + } else { + this.$bucket.deleteDialog(); + } } - none() { - // + closeSidePanels() { + this.$layout.closeSidePanels(); } - properties() { - // + openProperties() { + this.$layout.openProperties(); } - transfers() { - // + openTransfers() { + this.$layout.openTransfers(); } /** @@ -56,12 +66,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 +83,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..72204e0 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,29 +49,29 @@ - + - Upload + Create Bucket + Upload - + - Create Bucket - Create Folder + Create Folder Delete Bucket Delete @@ -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;