diff --git a/app/partials/common.jade b/app/partials/common.jade index 8c65256be8..636624496f 100644 --- a/app/partials/common.jade +++ b/app/partials/common.jade @@ -1,4 +1,3 @@ .left-nav(ui-view="left", ng-class="{toggled: menu.isCollapsed}") .top-view(ui-view="top") .main-container(ui-view="right", scroll-to-top, in-view-container, ng-click="hideMenu()") -downloader diff --git a/app/partials/export-history.jade b/app/partials/export-history.jade index dce506da1f..a6aeec2665 100644 --- a/app/partials/export-history.jade +++ b/app/partials/export-history.jade @@ -5,10 +5,10 @@ form(role="form" name="exportForm" ng-submit="submit()" autocomplete="off" noval helper-button(content="EXPORT_HISTORY_EXPLAIN") .modal-body - .ph-form - .flex-row.pal - .flex-1 - .flex-4.form-group(ng-show="activeCount > 1") + .ph-form.flex-row + .flex-1 + .flex-4 + .form-group.pal(ng-show="activeCount > 1") ui-select.send-from-dropdown( name="active" ng-model="$parent.active" @@ -18,10 +18,7 @@ form(role="form" name="exportForm" ng-submit="submit()" autocomplete="off" noval label-origin(origin="$select.selected") ui-select-choices(repeat="t in targets | filter:{label:$select.search} | limitTo:limit") label-origin(origin="::t" in-view="$last && isLast(t) && incLimit()" highlight="$select.search") - .flex-1 - - .flex-row - .flex-3.flex-column.flex-center.flex-around.pam(ng-class="{'has-error':exportForm.$invalid || start.date > end.date}") + .flex-row.flex-center.flex-between.pal(ng-class="{'has-error':exportForm.$invalid || start.date > end.date}") p.input-group.flex-center input.form-control( type="text" @@ -33,7 +30,7 @@ form(role="form" name="exportForm" ng-submit="submit()" autocomplete="off" noval placeholder="{{'START_DATE'|translate}}" required) span.ti-calendar.pointer(ng-click="start.open=true") - i.ti-arrow-down.blue + i.ti-arrow-right.blue p.input-group.flex-center input.form-control( type="text" @@ -45,14 +42,8 @@ form(role="form" name="exportForm" ng-submit="submit()" autocomplete="off" noval placeholder="{{'END_DATE'|translate}}" required) span.ti-calendar.pointer(ng-click="end.open=true") + .flex-1 - span.line-split.flex-center.mrl.mll - - .flex-3.flex-row.flex-center.flex-around.pam - label.mll.btn.button-default(ng-model="exportFormat" uib-btn-radio="'csv'") CSV - span.upper(translate="OR") - label.mrl.btn.button-default(ng-model="exportFormat" uib-btn-radio="'xls'") XLS - .modal-footer button.button-muted.mrm( type="button" @@ -63,4 +54,9 @@ form(role="form" name="exportForm" ng-submit="submit()" autocomplete="off" noval ui-ladda="busy" ladda-translate="EXPORT" data-style="expand-left" + ng-show="history == null || canTriggerDownload" ng-disabled="exportForm.$invalid || start.date > end.date") + download-button.btn.button-success( + ng-hide="history == null || canTriggerDownload" + filename="history.csv" + content="history") diff --git a/assets/js/controllers/exportHistory.controller.js b/assets/js/controllers/exportHistory.controller.js index b3c02e6bf4..79e5db494a 100644 --- a/assets/js/controllers/exportHistory.controller.js +++ b/assets/js/controllers/exportHistory.controller.js @@ -6,6 +6,9 @@ function ExportHistoryController ($scope, $sce, $translate, $filter, format, Wal $scope.limit = 50; $scope.incLimit = () => $scope.limit += 50; + $scope.ableBrowsers = ['chrome', 'firefox']; + $scope.canTriggerDownload = $scope.ableBrowsers.indexOf(browserDetection().browser) > -1; + let accounts = Wallet.accounts().filter(a => !a.archived && a.index != null); let addresses = Wallet.legacyAddresses().filter(a => !a.archived).map(a => a.address); @@ -37,7 +40,6 @@ function ExportHistoryController ($scope, $sce, $translate, $filter, format, Wal $scope.format = 'dd/MM/yyyy'; $scope.options = { minDate: new Date(1231024500000), maxDate: new Date() }; - $scope.exportFormat = 'csv'; $scope.start = { open: false, date: Date.now() - 604800000 }; $scope.end = { open: false, date: Date.now() }; @@ -48,6 +50,11 @@ function ExportHistoryController ($scope, $sce, $translate, $filter, format, Wal let start = $scope.formatDate($scope.start.date); let end = $scope.formatDate($scope.end.date); let active = $scope.active.address || $scope.active.xpub; - Wallet.exportHistory(start, end, active).finally(() => $scope.busy = false); + Wallet.exportHistory(start, end, active) + .then((data) => { + $scope.history = data; + $scope.canTriggerDownload && $scope.$broadcast('download'); + }) + .finally(() => $scope.busy = false); }; } diff --git a/assets/js/directives/download-button.directive.js b/assets/js/directives/download-button.directive.js new file mode 100644 index 0000000000..055aee4224 --- /dev/null +++ b/assets/js/directives/download-button.directive.js @@ -0,0 +1,28 @@ +angular + .module('walletApp') + .directive('downloadButton', downloadButton); + +function downloadButton ($window, $timeout) { + const directive = { + restrict: 'E', + replace: true, + scope: { + filename: '@', + content: '=' + }, + template: '{{::"DOWNLOAD"|translate}}', + link: link + }; + return directive; + + function link (scope, attr, elem) { + scope.$watch('content', (content) => { + let blob = new $window.Blob([content], {type: 'text/csv'}); + scope.dataRef = $window.URL.createObjectURL(blob); + }); + scope.$on('download', (event) => { + if (!scope.dataRef) return; + $timeout(() => elem.$$element[0].click()); + }); + } +} diff --git a/assets/js/directives/downloader.directive.js b/assets/js/directives/downloader.directive.js deleted file mode 100644 index d10038ae4f..0000000000 --- a/assets/js/directives/downloader.directive.js +++ /dev/null @@ -1,23 +0,0 @@ -angular - .module('walletApp') - .directive('downloader', downloader); - -function downloader ($window, $timeout) { - const directive = { - restrict: 'E', - replace: true, - template: '', - link: link - }; - return directive; - - function link (scope, attr, elem) { - scope.$on('download', (event, data) => { - if (!data.contents || !data.filename) return; - scope.blob = new $window.Blob([data.contents]); - scope.dataRef = $window.URL.createObjectURL(scope.blob); - scope.filename = data.filename; - $timeout(() => elem.$$element[0].click()); - }); - } -} diff --git a/assets/js/services/wallet.service.js b/assets/js/services/wallet.service.js index c73ca092a0..6c0e6a6814 100644 --- a/assets/js/services/wallet.service.js +++ b/assets/js/services/wallet.service.js @@ -1131,15 +1131,15 @@ function Wallet ($http, $window, $timeout, $location, Alerts, MyWallet, MyBlockc return $q.resolve(p) .then(history => { if (!history.length) return $q.reject('NO_HISTORY'); - let contents = json2csv(history.map(addTxNote)); - $rootScope.$broadcast('download', { contents, filename: 'history.csv' }); + return json2csv(history.map(addTxNote)); }) .catch(e => { let error = e.message || e; - if (error && error.indexOf('Too many transactions') > -1) { + if (typeof error === 'string' && error.indexOf('Too many transactions') > -1) { error = 'TOO_MANY_TXS'; } Alerts.displayError(error || 'UNKNOWN_ERROR'); + return $q.reject(error); }); }; diff --git a/locales/en-human.json b/locales/en-human.json index bcb586af9c..6f4eef306d 100644 --- a/locales/en-human.json +++ b/locales/en-human.json @@ -270,6 +270,7 @@ "EXPORTING_FOR" : "Exporting transactions for: {{ name }}", "NO_HISTORY" : "No transactions found in that range", "TOO_MANY_TXS" : "Too many transactions to export, maximum of 10,000 allowed", + "DOWNLOAD": "Download File", "SPENDABLE_ADDRESSES" : "Spendable Addresses", "NEXT_ADDRESSES_FOR_ACCOUNT" : "Unused Addresses in {{account}}", "PAST_ADDRESSES_FOR_ACCOUNT" : "Used Addresses in {{account}}", diff --git a/tests/directives/download-button_spec.coffee b/tests/directives/download-button_spec.coffee new file mode 100644 index 0000000000..052473cbfa --- /dev/null +++ b/tests/directives/download-button_spec.coffee @@ -0,0 +1,39 @@ +describe "downloadButton", -> + scope = undefined + element = undefined + isoScope = undefined + $timeout = undefined + + beforeEach module("walletApp") + + beforeEach inject(($compile, $rootScope, $window, _$timeout_) -> + $timeout = _$timeout_ + + spyOn($window, 'Blob').and.callFake((data) -> toString: -> "data[#{data.join()}]") + spyOn($window.URL, 'createObjectURL').and.callFake((obj) -> "blob://#{obj}") + + scope = $rootScope.$new() + scope.content = 'asdf' + scope.$digest() + + element = $compile("")(scope) + isoScope = element.isolateScope() + isoScope.$digest() + ) + + it "should create a data ref for content", -> + expect(isoScope.dataRef).toEqual('blob://data[asdf]') + + it "should create a data ref when content is updated", -> + isoScope.content = 'abc' + isoScope.$digest() + expect(isoScope.dataRef).toEqual('blob://data[abc]') + + it "should use the correct filename", -> + expect(isoScope.filename).toEqual('test.txt') + + it "should click the anchor tag to trigger download", -> + spyOn(element[0], 'click') + scope.$broadcast("download") + $timeout.flush() + expect(element[0].click).toHaveBeenCalled() diff --git a/tests/directives/downloader_spec.coffee b/tests/directives/downloader_spec.coffee deleted file mode 100644 index 74e4e31a02..0000000000 --- a/tests/directives/downloader_spec.coffee +++ /dev/null @@ -1,33 +0,0 @@ -describe "downloader", -> - element = undefined - scope = undefined - $rootScope = undefined - $window = undefined - $timeout = undefined - - beforeEach module("walletApp") - - beforeEach inject(($compile, _$rootScope_, _$window_, _$timeout_) -> - $rootScope = _$rootScope_ - $window = _$window_ - $timeout = _$timeout_ - - scope = $rootScope.$new() - element = $compile("")(scope) - ) - - beforeEach -> - spyOn($window, 'Blob').and.callFake((data) -> toString: -> "blob[#{data.join()}]") - spyOn($window.URL, 'createObjectURL').and.callFake((obj) -> "ref://#{obj}") - $rootScope.$broadcast('download', { contents: 'asdf', filename: 'test.txt' }) - - it "should create a data ref on receiving download event", -> - expect(scope.dataRef).toEqual('ref://blob[asdf]') - - it "should use the correct filename", -> - expect(scope.filename).toEqual('test.txt') - - it "should click the anchor tag to trigger download", -> - spyOn(element[0], 'click') - $timeout.flush() - expect(element[0].click).toHaveBeenCalled() diff --git a/tests/services/wallet_service_spec.coffee b/tests/services/wallet_service_spec.coffee index 7fe19e5e52..c909c7b839 100644 --- a/tests/services/wallet_service_spec.coffee +++ b/tests/services/wallet_service_spec.coffee @@ -529,10 +529,8 @@ describe "walletServices", () -> it "should convert to csv with notes and broadcast broadcast download event", (done) -> spyOn(Wallet, 'getNote').and.callFake((hash) -> hash == 'asdf' && 'test_note') spyOn($rootScope, '$broadcast') - Wallet.exportHistory().then -> - expect($rootScope.$broadcast).toHaveBeenCalledWith 'download', - contents: 'sent,receive,tx,note\n1,0,asdf,test_note\n0,2,qwer,' - filename: 'history.csv' + Wallet.exportHistory().then (data) -> + expect(data).toEqual('sent,receive,tx,note\n1,0,asdf,test_note\n0,2,qwer,') done() $rootScope.$digest() @@ -541,7 +539,7 @@ describe "walletServices", () -> spyOn(MyBlockchainApi, 'exportHistory').and.returnValue([]) it "should show an error", (done) -> - Wallet.exportHistory().then -> + Wallet.exportHistory().finally -> expect(Alerts.displayError).toHaveBeenCalledWith('NO_HISTORY') done() $rootScope.$digest()