From 9822510051567b6c705901dbdf2c77a863d31e5e Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Wed, 2 Dec 2015 12:06:55 +0530 Subject: [PATCH 1/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Added Edit header to the external data cards --- .../external-link-data.directive.jade | 8 +++-- .../external-links-data.directive.js | 3 +- .../onoffswitch/onoffswitch.directive.jade | 5 +++ .../onoffswitch/onoffswitch.directive.js | 19 ++++++++++ app/index.jade | 1 + app/settings/edit-profile/edit-profile.jade | 2 +- assets/css/directives/external-link-data.scss | 36 ++++++++++++++++++- 7 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 app/directives/onoffswitch/onoffswitch.directive.jade create mode 100644 app/directives/onoffswitch/onoffswitch.directive.js diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index 77a486ffe..75a8e08d2 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -1,5 +1,9 @@ .external-link-list - div.external-link-tile(ng-repeat="account in linkedAccountsData") + div.external-link-tile(ng-repeat="account in linkedAccountsData", ng-class="{'external-link-tile--editable' : editable}") + .ext-link-tile_edit-header(ng-show="editable") + .ext-link-tile_edit-header_show-on-profile + span.show-on-profile_label Show on Profile + onoff-switch(model="account.hide", unique-id="account.provider + '_' + (account.key || account.data.handle)") .top div.logo i.fa(ng-class="(account|providerData:'className') || 'fa-globe'") @@ -98,7 +102,7 @@ .title {{account.data.title}} - div(ng-switch-when="weblink") + div(ng-switch-when="weblink", key="{{account.key}}") p.link-title(data-ellipsis, ng-bind="account.title", ng-hide="account.status === 'PENDING'") p.link-title(ng-show="account.status === 'PENDING'") Loading data. This will take a few minutes. diff --git a/app/directives/external-account/external-links-data.directive.js b/app/directives/external-account/external-links-data.directive.js index fdb77f690..cfa5a79d8 100644 --- a/app/directives/external-account/external-links-data.directive.js +++ b/app/directives/external-account/external-links-data.directive.js @@ -14,7 +14,8 @@ restrict: 'E', templateUrl: 'directives/external-account/external-link-data.directive.html', scope: { - linkedAccountsData: '=' + linkedAccountsData: '=', + editable: '=' } }; return directive; diff --git a/app/directives/onoffswitch/onoffswitch.directive.jade b/app/directives/onoffswitch/onoffswitch.directive.jade new file mode 100644 index 000000000..c082c08b8 --- /dev/null +++ b/app/directives/onoffswitch/onoffswitch.directive.jade @@ -0,0 +1,5 @@ +.onoffswitch + input.onoffswitch-checkbox(type='checkbox', name='onoffswitch', checked='', ng-model="model", id="{{uniqueId}}-onoffswitch") + label.onoffswitch-label(for='{{uniqueId}}-onoffswitch') + span.onoffswitch-inner + span.onoffswitch-switch \ No newline at end of file diff --git a/app/directives/onoffswitch/onoffswitch.directive.js b/app/directives/onoffswitch/onoffswitch.directive.js new file mode 100644 index 000000000..596ffc0e1 --- /dev/null +++ b/app/directives/onoffswitch/onoffswitch.directive.js @@ -0,0 +1,19 @@ +(function() { + 'use strict'; + + angular.module('tcUIComponents').directive('onoffSwitch', onoffSwitch); + + function onoffSwitch() { + return { + restrict: 'E', + templateUrl: 'directives/onoffswitch/onoffswitch.directive.html', + scope: { + model: '=', + uniqueId: '=' + }, + link: function(scope, element, attrs) { + + } + }; + } +})(); diff --git a/app/index.jade b/app/index.jade index ed2cf125e..390451e0e 100644 --- a/app/index.jade +++ b/app/index.jade @@ -201,6 +201,7 @@ html script(src="directives/input-sticky-placeholder/input-sticky-placeholder.directive.js") script(src="directives/ios-card/ios-card.directive.js") script(src="directives/on-file-change.directive.js") + script(src="directives/onoffswitch/onoffswitch.directive.js") script(src="directives/page-state-header/page-state-header.directive.js") script(src="directives/profile-widget/profile-widget.directive.js") script(src="directives/responsive-carousel/responsive-carousel.directive.js") diff --git a/app/settings/edit-profile/edit-profile.jade b/app/settings/edit-profile/edit-profile.jade index 4b977b46f..896278013 100644 --- a/app/settings/edit-profile/edit-profile.jade +++ b/app/settings/edit-profile/edit-profile.jade @@ -108,4 +108,4 @@ .field-label Linked Accounts .existing-links - external-links-data(linked-accounts-data="vm.linkedExternalAccountsData") + external-links-data(linked-accounts-data="vm.linkedExternalAccountsData", editable="true") diff --git a/assets/css/directives/external-link-data.scss b/assets/css/directives/external-link-data.scss index 832476103..0a14c9dfb 100644 --- a/assets/css/directives/external-link-data.scss +++ b/assets/css/directives/external-link-data.scss @@ -5,6 +5,36 @@ external-accounts { flex-flow: row wrap; } +.ext-link-tile_edit-header { + border-bottom: 1px solid $gray-light; + + &:hover { + .show-on-profile_label { + color: $accent-gray-dark; + } + } +} + +.ext-link-tile_edit-header_show-on-profile { + display: flex; + justify-content: space-between; + padding: 10px 10px; + + .show-on-profile_label { + @include sofia-pro-medium; + font-size: 10px; + line-height: 13px; + color: $accent-gray; + text-transform: uppercase; + align-self: center; + } + + // TODO remove this and handle in onoffswitch directive's css + .onoffswitch { + margin: 0px; + } +} + .external-link-list { display: flex; flex-direction: column; @@ -12,7 +42,7 @@ external-accounts { margin-top: 10px; .external-link-tile { - border: 1px solid #f0f0f0; + border: 1px solid $gray-light; width: 280px; height: 90px; display: flex; @@ -252,5 +282,9 @@ external-accounts { } } } + + .external-link-tile--editable { + height: 285px; + } } } From 5d3b50971ea390422986f38adc76a150e176869a Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Fri, 4 Dec 2015 21:21:58 +0530 Subject: [PATCH 2/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Removed toggle switch for hiding the profile -- Added trash icon for deleting the link -- Added unit tests for link deletion flow -- Updated unit tests for externalWeblinksService for removeLink method --- .../external-link-data.directive.jade | 4 +- .../external-links-data.directive.js | 47 ++++- .../external-links-data.directive.spec.js | 199 +++++++++++++----- app/services/externalLinks.service.js | 18 +- app/services/externalLinks.service.spec.js | 44 +++- app/settings/edit-profile/edit-profile.jade | 2 +- assets/css/directives/external-link-data.scss | 18 ++ assets/images/ico-delete.svg | 9 + 8 files changed, 276 insertions(+), 65 deletions(-) create mode 100644 assets/images/ico-delete.svg diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index 75a8e08d2..2ed3ee883 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -1,9 +1,7 @@ .external-link-list div.external-link-tile(ng-repeat="account in linkedAccountsData", ng-class="{'external-link-tile--editable' : editable}") .ext-link-tile_edit-header(ng-show="editable") - .ext-link-tile_edit-header_show-on-profile - span.show-on-profile_label Show on Profile - onoff-switch(model="account.hide", unique-id="account.provider + '_' + (account.key || account.data.handle)") + .ext-link-tile_edit-header_delete(ng-click="deleteAccount(account)", ng-class="{'ext-link-tile_edit-header_delete--deleting': deletingAccount}") .top div.logo i.fa(ng-class="(account|providerData:'className') || 'fa-globe'") diff --git a/app/directives/external-account/external-links-data.directive.js b/app/directives/external-account/external-links-data.directive.js index cfa5a79d8..da7cc839a 100644 --- a/app/directives/external-account/external-links-data.directive.js +++ b/app/directives/external-account/external-links-data.directive.js @@ -15,8 +15,51 @@ templateUrl: 'directives/external-account/external-link-data.directive.html', scope: { linkedAccountsData: '=', - editable: '=' - } + editable: '=', + userHandle: '@' + }, + controller: ['$log', '$scope', 'ExternalWebLinksService', 'toaster', + function($log, $scope, ExternalWebLinksService, toaster) { + + $log = $log.getInstance("ExternalLinksDataCtrl"); + $scope.deletingAccount = false; + + $scope.deleteAccount = function(account) { + $log.debug('Deleting Account...'); + if ($scope.deletingAccount) { + $log.debug('Another deletion is already in progress.'); + return; + } + if (account && account.provider === 'weblink') { + $log.debug('Deleting weblink...'); + $scope.deletingAccount = true; + ExternalWebLinksService.removeLink($scope.userHandle, account.key).then(function(data) { + $scope.deletingAccount = false; + $log.debug("Web link removed: " + JSON.stringify(data)); + var toRemove = _.findIndex($scope.linkedAccountsData, function(la) { + return la.provider === 'weblink' && la.key === account.key; + }); + if (toRemove > -1) { + // remove from the linkedAccountsData array + $scope.linkedAccountsData.splice(toRemove, 1); + } + toaster.pop('success', "Success", "Your link has been added. Data from your link will be visible on your profile shortly."); + }) + .catch(function(resp) { + var msg = resp.msg; + if (resp.status === 'WEBLINK_NOT_EXIST') { + $log.info("Weblink does not exist"); + msg = "Weblink is not linked to your account. If you think this is an error please contact support@topcoder.com."; + } else { + $log.error("Fatal error: _unlink: " + msg); + msg = "Sorry! We are unable to remove your weblink. If problem persists, please contact support@topcoder.com"; + } + toaster.pop('error', "Whoops!", msg); + }); + } + } + } + ] }; return directive; } diff --git a/app/directives/external-account/external-links-data.directive.spec.js b/app/directives/external-account/external-links-data.directive.spec.js index 81f085ef9..390c114d2 100644 --- a/app/directives/external-account/external-links-data.directive.spec.js +++ b/app/directives/external-account/external-links-data.directive.spec.js @@ -2,84 +2,169 @@ describe('External Links Data Directive', function() { var scope; var element; + var toasterSvc, extLinkSvc; + var mockLinkedAccounts = [ + { + provider: 'github', + data: { + handle: "github-handle", + followers: 1, + publicRepos: 1 + } + }, + { provider: 'stackoverflow', + data: { + handle: 'so-handle', + reputation: 2, + answers: 2 + } + }, + { + provider: 'behance', + data: { + name: 'behance name', + projectViews: 3, + projectAppreciations: 3 + } + }, + { + provider: 'dribbble', + data: { + handle: 'dribbble-handle', + followers: 4, + likes: 4 + } + }, + { + provider: 'bitbucket', + data: { + username: 'bitbucket-username', + followers: 5, + repositories: 5 + } + }, + { + provider: 'twitter', + data: { + handle: 'twitter-handle', + noOfTweets: 6, + followers: 6 + } + }, + { + provider: 'linkedin', + data: { + status: 'pending' + } + }, + { + provider: 'weblink', + key: 'somekey' + } + ]; beforeEach(function() { bard.appModule('topcoder'); - bard.inject(this, '$compile', '$rootScope'); + bard.inject(this, '$compile', '$rootScope', 'toaster', 'ExternalWebLinksService', '$q'); scope = $rootScope.$new(); + + extLinkSvc = ExternalWebLinksService; + + sinon.stub(extLinkSvc, 'removeLink', function(handle, key) { + var $deferred = $q.defer(); + if (key === 'throwNotExistsError') { + $deferred.reject({ + status: 'WEBLINK_NOT_EXIST', + msg: 'profile not exists' + }); + } else if(key === 'throwFatalError') { + $deferred.reject({ + status: 'FATAL_ERROR', + msg: 'fatal error' + }); + } else { + $deferred.resolve({ + status: 'SUCCESS' + }); + } + return $deferred.promise; + }); + + toasterSvc = toaster; + bard.mockService(toaster, { + pop: $q.when(true), + default: $q.when(true) + }); }); bard.verifyNoOutstandingHttpRequests(); describe('Linked external accounts', function() { - var linkedAccounts = [ - { - provider: 'github', - data: { - handle: "github-handle", - followers: 1, - publicRepos: 1 - } - }, - { provider: 'stackoverflow', - data: { - handle: 'so-handle', - reputation: 2, - answers: 2 - } - }, - { - provider: 'behance', - data: { - name: 'behance name', - projectViews: 3, - projectAppreciations: 3 - } - }, - { - provider: 'dribbble', - data: { - handle: 'dribbble-handle', - followers: 4, - likes: 4 - } - }, - { - provider: 'bitbucket', - data: { - username: 'bitbucket-username', - followers: 5, - repositories: 5 - } - }, - { - provider: 'twitter', - data: { - handle: 'twitter-handle', - noOfTweets: 6, - followers: 6 - } - }, - { - provider: 'linkedin', - data: { - status: 'pending' - } - } - ]; + var linkedAccounts = null; var externalLinksData; beforeEach(function() { + linkedAccounts = angular.copy(mockLinkedAccounts); scope.linkedAccounts = linkedAccounts; - element = angular.element(')'); + element = angular.element(')'); externalLinksData = $compile(element)(scope); scope.$digest(); }); + afterEach(function() { + linkedAccounts = angular.copy(mockLinkedAccounts); + scope.linkedAccounts = linkedAccounts; + }); + it('should have added linkedAccounts to scope', function() { expect(element.isolateScope().linkedAccountsData).to.exist; + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + }); + + it('should remove weblink ', function() { + element.isolateScope().deleteAccount({key: 'somekey', provider: 'weblink'}); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; expect(element.isolateScope().linkedAccountsData).to.have.length(7); }); + it('should show success if controller doesn\'t have weblink but API returns success ', function() { + element.isolateScope().deleteAccount({key: 'somekey1', provider: 'weblink'}); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + }); + + it('should NOT remove weblink with fatal error ', function() { + element.isolateScope().deleteAccount({key: 'throwFatalError', provider: 'weblink'}); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('Sorry!')).calledOnce; + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + }); + + it('should NOT remove weblink with already removed weblink ', function() { + element.isolateScope().deleteAccount({key: 'throwNotExistsError', provider: 'weblink'}); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('not linked')).calledOnce; + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + }); + + it('should not do any thing when already a deletion is in progress ', function() { + element.isolateScope().deletingAccount = true; + element.isolateScope().deleteAccount({key: 'somekey', provider: 'weblink'}); + scope.$digest(); + expect(extLinkSvc.removeLink).not.to.be.called; + expect(toasterSvc.pop).not.to.be.called; + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + }); + + it('should not do any thing for non weblink provider ', function() { + element.isolateScope().deleteAccount({key: 'somekey', provider: 'stackoverflow'}); + scope.$digest(); + expect(extLinkSvc.removeLink).not.to.be.called; + expect(toasterSvc.pop).not.to.be.called; + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + }); + }); }); diff --git a/app/services/externalLinks.service.js b/app/services/externalLinks.service.js index 8c8437401..d8fcc2bb8 100644 --- a/app/services/externalLinks.service.js +++ b/app/services/externalLinks.service.js @@ -67,7 +67,23 @@ } function removeLink(userHandle, key) { - return memberApi.one('members', userHandle).one('externalLinks', key).remove(); + return $q(function($resolve, $reject) { + return memberApi.one('members', userHandle).one('externalLinks', key).remove() + .then(function(resp) { + $resolve(resp); + }) + .catch(function(resp) { + var errorStatus = "FATAL_ERROR"; + $log.error("Error removing weblink: " + resp.data.result.content); + if (resp.data.result && resp.data.result.status === 400) { + errorStatus = "WEBLINK_NOT_EXIST"; + } + $reject({ + status: errorStatus, + msg: resp.data.result.content + }); + }); + }); } } diff --git a/app/services/externalLinks.service.spec.js b/app/services/externalLinks.service.spec.js index 0b228456a..06c7d6c01 100644 --- a/app/services/externalLinks.service.spec.js +++ b/app/services/externalLinks.service.spec.js @@ -64,10 +64,24 @@ describe('ExternalWebLinks service', function() { $httpBackend.flush(); }); + it('should fail, with fatal error, in adding link', function() { + var errorMessage = "endpoint not found"; + linksPost.respond(404, {result: { status: 404, content: errorMessage } }); + // call addLink method with valid args + service.addLink('test1', "http://google.com").then(function(data) { + sinon.assert.fail('should not be called'); + }).catch(function(error) { + expect(error).to.be.defined; + expect(error.status).to.exist.to.equal('FATAL_ERROR'); + expect(error.msg).to.exist.to.equal(errorMessage); + }); + $httpBackend.flush(); + }); + it('should fail with already existing link', function() { var errorMessage = "web link exists"; linksPost.respond(400, {result: { status: 400, content: errorMessage } }); - // call linkExternalAccount method, having user service throw already exist + // call addLink method, having user service throw already exist service.addLink('test1', "http://google.com").then(function(data) { sinon.assert.fail('should not be called'); }, function(error) { @@ -87,4 +101,32 @@ describe('ExternalWebLinks service', function() { $httpBackend.flush(); }); + it('should fail with non existing link', function() { + var errorMessage = "web link does not exists"; + linksDelete.respond(400, {result: { status: 400, content: errorMessage } }); + // call removeLink method + service.removeLink('test1', "testkey").then(function(data) { + sinon.assert.fail('should not be called'); + }, function(error) { + expect(error).to.be.defined; + expect(error.status).to.exist.to.equal('WEBLINK_NOT_EXIST'); + expect(error.msg).to.exist.to.equal(errorMessage); + }); + $httpBackend.flush(); + }); + + it('should fail, with fatal error, in removing link', function() { + var errorMessage = "endpoint not found"; + linksDelete.respond(404, {result: { status: 404, content: errorMessage } }); + // call removeLink method with valid args + service.removeLink('test1', "testkey").then(function(data) { + sinon.assert.fail('should not be called'); + }).catch(function(error) { + expect(error).to.be.defined; + expect(error.status).to.exist.to.equal('FATAL_ERROR'); + expect(error.msg).to.exist.to.equal(errorMessage); + }); + $httpBackend.flush(); + }); + }); diff --git a/app/settings/edit-profile/edit-profile.jade b/app/settings/edit-profile/edit-profile.jade index 896278013..56d63a3bc 100644 --- a/app/settings/edit-profile/edit-profile.jade +++ b/app/settings/edit-profile/edit-profile.jade @@ -108,4 +108,4 @@ .field-label Linked Accounts .existing-links - external-links-data(linked-accounts-data="vm.linkedExternalAccountsData", editable="true") + external-links-data(linked-accounts-data="vm.linkedExternalAccountsData", editable="true", user-handle="{{vm.userData.handle}}") diff --git a/assets/css/directives/external-link-data.scss b/assets/css/directives/external-link-data.scss index 0a14c9dfb..922e6547e 100644 --- a/assets/css/directives/external-link-data.scss +++ b/assets/css/directives/external-link-data.scss @@ -7,6 +7,8 @@ external-accounts { .ext-link-tile_edit-header { border-bottom: 1px solid $gray-light; + display: flex; + justify-content: flex-end; &:hover { .show-on-profile_label { @@ -15,6 +17,22 @@ external-accounts { } } +.ext-link-tile_edit-header_delete { + border-left: 1px solid $gray-light; + background-image: url(/images/ico-delete.svg); + background-position: center; + background-size: 16px 16px; + background-repeat: no-repeat; + height: 43px; + width: 51px; + cursor: pointer; +} + +.ext-link-tile_edit-header_delete--deleting { + cursor: default; + opacity: 0.5; +} + .ext-link-tile_edit-header_show-on-profile { display: flex; justify-content: space-between; diff --git a/assets/images/ico-delete.svg b/assets/images/ico-delete.svg new file mode 100644 index 000000000..ebb151dfe --- /dev/null +++ b/assets/images/ico-delete.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file From 140d7a773f4240537fbc2fd674f9a4959379d424 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 8 Dec 2015 11:32:51 +0530 Subject: [PATCH 3/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Disabled the remove link for PENDING state card --- .../external-account/external-link-data.directive.jade | 2 +- assets/css/directives/external-link-data.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index 2ed3ee883..51950b3b1 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -1,7 +1,7 @@ .external-link-list div.external-link-tile(ng-repeat="account in linkedAccountsData", ng-class="{'external-link-tile--editable' : editable}") .ext-link-tile_edit-header(ng-show="editable") - .ext-link-tile_edit-header_delete(ng-click="deleteAccount(account)", ng-class="{'ext-link-tile_edit-header_delete--deleting': deletingAccount}") + .ext-link-tile_edit-header_delete(ng-click="deleteAccount(account)", ng-class="{'ext-link-tile_edit-header_delete--disabled': deletingAccount || account.status === 'PENDING'}") .top div.logo i.fa(ng-class="(account|providerData:'className') || 'fa-globe'") diff --git a/assets/css/directives/external-link-data.scss b/assets/css/directives/external-link-data.scss index 922e6547e..016ca83cd 100644 --- a/assets/css/directives/external-link-data.scss +++ b/assets/css/directives/external-link-data.scss @@ -28,7 +28,7 @@ external-accounts { cursor: pointer; } -.ext-link-tile_edit-header_delete--deleting { +.ext-link-tile_edit-header_delete--disabled { cursor: default; opacity: 0.5; } From 9d14022409ba6eb77429a9b9ce9596fd69e9840c Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 8 Dec 2015 16:10:22 +0530 Subject: [PATCH 4/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Added confirmation popup before deleting the weblink -- Updated existing tests for the changes made for confirmation pop up -- Added new tests for confirmation popup functionality --- .../external-link-data.directive.jade | 6 +- .../external-link-deletion-confirm.jade | 8 +++ .../external-links-data.directive.js | 33 ++++++--- .../external-links-data.directive.spec.js | 72 ++++++++++++++++--- app/index.jade | 1 + .../external-link-deletion-confirm.scss | 49 +++++++++++++ 6 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 app/directives/external-account/external-link-deletion-confirm.jade create mode 100644 assets/css/directives/external-link-deletion-confirm.scss diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index 51950b3b1..45a52236b 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -1,13 +1,15 @@ .external-link-list div.external-link-tile(ng-repeat="account in linkedAccountsData", ng-class="{'external-link-tile--editable' : editable}") .ext-link-tile_edit-header(ng-show="editable") - .ext-link-tile_edit-header_delete(ng-click="deleteAccount(account)", ng-class="{'ext-link-tile_edit-header_delete--disabled': deletingAccount || account.status === 'PENDING'}") + .ext-link-tile_edit-header_delete(ng-click="confirmDeletion(account)", ng-class="{'ext-link-tile_edit-header_delete--disabled': account.deletingAccount || account.status === 'PENDING'}") .top div.logo i.fa(ng-class="(account|providerData:'className') || 'fa-globe'") h2 {{account|providerData:"displayName"}} - div.bottom(ng-switch="account.provider") + div.bottom(ng-if="account.deletingAccount") + .section-loading + div.bottom(ng-switch="account.provider", ng-if="!account.deletingAccount") div(ng-switch-when="github") .handle {{account.data.handle}} diff --git a/app/directives/external-account/external-link-deletion-confirm.jade b/app/directives/external-account/external-link-deletion-confirm.jade new file mode 100644 index 000000000..2667c4b73 --- /dev/null +++ b/app/directives/external-account/external-link-deletion-confirm.jade @@ -0,0 +1,8 @@ +.deletion-confirmation + .deletion-confirmation-title Heads Up! + .deletion-confirmation-message Are you sure you want to delete the external link? This action can't be undone later + .deletion-confirmation-buttons + .deletion-confirmation-button-yes + button.tc-btn.tc-btn-s.tc-btn-ghost(ng-click="deleteAccount() && closeThisDialog()") Yes, Delete Link + .deletion-confirmation-button-no + button.tc-btn.tc-btn-s(ng-click="closeThisDialog()") Cancel \ No newline at end of file diff --git a/app/directives/external-account/external-links-data.directive.js b/app/directives/external-account/external-links-data.directive.js index da7cc839a..8f0a4ac37 100644 --- a/app/directives/external-account/external-links-data.directive.js +++ b/app/directives/external-account/external-links-data.directive.js @@ -18,22 +18,36 @@ editable: '=', userHandle: '@' }, - controller: ['$log', '$scope', 'ExternalWebLinksService', 'toaster', - function($log, $scope, ExternalWebLinksService, toaster) { + controller: ['$log', '$scope', 'ExternalWebLinksService', 'toaster', 'ngDialog', + function($log, $scope, ExternalWebLinksService, toaster, ngDialog) { $log = $log.getInstance("ExternalLinksDataCtrl"); - $scope.deletingAccount = false; + $scope.toDelete = null; + $scope.deletionDialog = null; - $scope.deleteAccount = function(account) { + $scope.confirmDeletion = function(account) { + $scope.toDelete = account; + $scope.deletionDialog = ngDialog.open({ + className: 'ngdialog-theme-default tc-dialog', + template: 'directives/external-account/external-link-deletion-confirm.html', + scope: $scope + }).closePromise.then(function (data) { + $log.debug('Closing deletion confirmation dialog.'); + $scope.toDelete = null; + }); + } + + $scope.deleteAccount = function() { $log.debug('Deleting Account...'); - if ($scope.deletingAccount) { + var account = $scope.toDelete; + if (account && account.deletingAccount) { $log.debug('Another deletion is already in progress.'); return; } if (account && account.provider === 'weblink') { + account.deletingAccount = true; $log.debug('Deleting weblink...'); - $scope.deletingAccount = true; - ExternalWebLinksService.removeLink($scope.userHandle, account.key).then(function(data) { + return ExternalWebLinksService.removeLink($scope.userHandle, account.key).then(function(data) { $scope.deletingAccount = false; $log.debug("Web link removed: " + JSON.stringify(data)); var toRemove = _.findIndex($scope.linkedAccountsData, function(la) { @@ -43,7 +57,8 @@ // remove from the linkedAccountsData array $scope.linkedAccountsData.splice(toRemove, 1); } - toaster.pop('success', "Success", "Your link has been added. Data from your link will be visible on your profile shortly."); + account.deletingAccount = false; + toaster.pop('success', "Success", "Your link has been removed."); }) .catch(function(resp) { var msg = resp.msg; @@ -54,6 +69,8 @@ $log.error("Fatal error: _unlink: " + msg); msg = "Sorry! We are unable to remove your weblink. If problem persists, please contact support@topcoder.com"; } + + account.deletingAccount = false; toaster.pop('error', "Whoops!", msg); }); } diff --git a/app/directives/external-account/external-links-data.directive.spec.js b/app/directives/external-account/external-links-data.directive.spec.js index 390c114d2..6aae6c220 100644 --- a/app/directives/external-account/external-links-data.directive.spec.js +++ b/app/directives/external-account/external-links-data.directive.spec.js @@ -2,7 +2,7 @@ describe('External Links Data Directive', function() { var scope; var element; - var toasterSvc, extLinkSvc; + var toasterSvc, extLinkSvc, ngDialogSvc; var mockLinkedAccounts = [ { provider: 'github', @@ -65,7 +65,7 @@ describe('External Links Data Directive', function() { beforeEach(function() { bard.appModule('topcoder'); - bard.inject(this, '$compile', '$rootScope', 'toaster', 'ExternalWebLinksService', '$q'); + bard.inject(this, '$compile', '$rootScope', 'toaster', 'ExternalWebLinksService', '$q', 'ngDialog'); scope = $rootScope.$new(); extLinkSvc = ExternalWebLinksService; @@ -95,6 +95,16 @@ describe('External Links Data Directive', function() { pop: $q.when(true), default: $q.when(true) }); + + ngDialogSvc = ngDialog; + sinon.stub(ngDialog, 'open', function() { + ngDialog.deferredClose = $q.defer(); + return { closePromise : ngDialog.deferredClose.promise }; + }); + sinon.stub(ngDialog, 'close', function() { + ngDialog.deferredClose.resolve('closing'); + return + }) }); bard.verifyNoOutstandingHttpRequests(); @@ -122,36 +132,40 @@ describe('External Links Data Directive', function() { }); it('should remove weblink ', function() { - element.isolateScope().deleteAccount({key: 'somekey', provider: 'weblink'}); + element.isolateScope().toDelete = {key: 'somekey', provider: 'weblink'}; + element.isolateScope().deleteAccount(); scope.$digest(); expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; expect(element.isolateScope().linkedAccountsData).to.have.length(7); }); it('should show success if controller doesn\'t have weblink but API returns success ', function() { - element.isolateScope().deleteAccount({key: 'somekey1', provider: 'weblink'}); + element.isolateScope().toDelete = {key: 'somekey1', provider: 'weblink'}; + element.isolateScope().deleteAccount(); scope.$digest(); expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; expect(element.isolateScope().linkedAccountsData).to.have.length(8); }); it('should NOT remove weblink with fatal error ', function() { - element.isolateScope().deleteAccount({key: 'throwFatalError', provider: 'weblink'}); + element.isolateScope().toDelete = {key: 'throwFatalError', provider: 'weblink'}; + element.isolateScope().deleteAccount(); scope.$digest(); expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('Sorry!')).calledOnce; expect(element.isolateScope().linkedAccountsData).to.have.length(8); }); it('should NOT remove weblink with already removed weblink ', function() { - element.isolateScope().deleteAccount({key: 'throwNotExistsError', provider: 'weblink'}); + element.isolateScope().toDelete = {key: 'throwNotExistsError', provider: 'weblink'}; + element.isolateScope().deleteAccount(); scope.$digest(); expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('not linked')).calledOnce; expect(element.isolateScope().linkedAccountsData).to.have.length(8); }); it('should not do any thing when already a deletion is in progress ', function() { - element.isolateScope().deletingAccount = true; - element.isolateScope().deleteAccount({key: 'somekey', provider: 'weblink'}); + element.isolateScope().toDelete = {key: 'somekey', provider: 'weblink', deletingAccount: true}; + element.isolateScope().deleteAccount(); scope.$digest(); expect(extLinkSvc.removeLink).not.to.be.called; expect(toasterSvc.pop).not.to.be.called; @@ -159,12 +173,52 @@ describe('External Links Data Directive', function() { }); it('should not do any thing for non weblink provider ', function() { - element.isolateScope().deleteAccount({key: 'somekey', provider: 'stackoverflow'}); + element.isolateScope().toDelete = {key: 'somekey', provider: 'stackoverflow'}; + element.isolateScope().deleteAccount(); scope.$digest(); expect(extLinkSvc.removeLink).not.to.be.called; expect(toasterSvc.pop).not.to.be.called; expect(element.isolateScope().linkedAccountsData).to.have.length(8); }); + it('should mark the account for deletion and open the popup ', function() { + var account = {key: 'somekey', provider: 'stackoverflow'}; + element.isolateScope().confirmDeletion(account); + scope.$digest(); + // just to cross check that it does not call removal service directly + expect(extLinkSvc.removeLink).not.to.be.called; + // should open the popup + expect(ngDialogSvc.open).to.be.called; + // should not remove anythign from the array of accounts/links + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + // $scope.toDelete should point to account passed to the confirmDeletion method + expect(element.isolateScope().toDelete).to.exist; + expect(element.isolateScope().toDelete.key).to.exist.to.equal(account.key); + expect(element.isolateScope().toDelete.provider).to.exist.to.equal(account.provider); + ngDialogSvc.close(); + }); + + it('should call closePromise after popup closing ', function() { + var account = {key: 'somekey', provider: 'stackoverflow'}; + element.isolateScope().confirmDeletion(account); + scope.$digest(); + // should open the popup + expect(ngDialogSvc.open).to.be.called; + // should not remove anythign from the array of accounts/links + expect(element.isolateScope().linkedAccountsData).to.have.length(8); + // $scope.toDelete should point to account passed to the confirmDeletion method + expect(element.isolateScope().toDelete).to.exist; + expect(element.isolateScope().toDelete.key).to.exist.to.equal(account.key); + expect(element.isolateScope().toDelete.provider).to.exist.to.equal(account.provider); + // adds resolve listener to the closePromise of ngDialog to verify that closePromise + // has been called after closing the ngDialog + ngDialogSvc.deferredClose.promise.then(function() { + // should reset scope.toDelete to null + expect(element.isolateScope().toDelete).to.null; + }); + // close dialog and resolve closePromise + ngDialogSvc.close(); + }); + }); }); diff --git a/app/index.jade b/app/index.jade index 390451e0e..a05ed744c 100644 --- a/app/index.jade +++ b/app/index.jade @@ -73,6 +73,7 @@ html link(rel="stylesheet", href="assets/css/directives/ios-card.css") link(rel="stylesheet", href="assets/css/directives/history-graph.css") link(rel="stylesheet", href="assets/css/directives/external-web-link.css") + link(rel="stylesheet", href="assets/css/directives/external-link-deletion-confirm.css") link(rel="stylesheet", href="assets/css/directives/external-link-data.css") link(rel="stylesheet", href="assets/css/directives/external-account.css") link(rel="stylesheet", href="assets/css/directives/empty-state-placeholder.css") diff --git a/assets/css/directives/external-link-deletion-confirm.scss b/assets/css/directives/external-link-deletion-confirm.scss new file mode 100644 index 000000000..c2c1377cc --- /dev/null +++ b/assets/css/directives/external-link-deletion-confirm.scss @@ -0,0 +1,49 @@ +@import 'topcoder/tc-includes'; + +.ngdialog.tc-dialog { + .ngdialog-content { + background: transparent; + opacity: 1; + } +} + +.deletion-confirmation { + display: flex; + flex-flow: column wrap; + justify-content: space-around; + align-items: center; + background-color: $white; + opacity: 1; + border-radius: 4px; + padding: 40px; +} + +.deletion-confirmation-title { + @include sofia-pro-medium; + font-size: 20px; + line-height: 24px; + color: $gray-darkest; + text-transform: uppercase; +} + +.deletion-confirmation-message { + @include merriweather-sans-regular;//TODO use sansbook font +} + +.deletion-confirmation-buttons { + display: flex; + flex-flow: row wrap; + + .deletion-confirmation-button-no { + margin-left: 10px; + } +} + + + +@media (min-width: 768px) { + .deletion-confirmation { + width: 520px; + height: 256px; + } +} \ No newline at end of file From 39051ac60c1968f992db1f69f962e84a1a6d81a8 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 8 Dec 2015 16:15:28 +0530 Subject: [PATCH 5/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Made the edit header visible only for weblink --- .../external-account/external-link-data.directive.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index 45a52236b..e9b9965b2 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -1,6 +1,6 @@ .external-link-list div.external-link-tile(ng-repeat="account in linkedAccountsData", ng-class="{'external-link-tile--editable' : editable}") - .ext-link-tile_edit-header(ng-show="editable") + .ext-link-tile_edit-header(ng-show="editable && account.provider === 'weblink'") .ext-link-tile_edit-header_delete(ng-click="confirmDeletion(account)", ng-class="{'ext-link-tile_edit-header_delete--disabled': account.deletingAccount || account.status === 'PENDING'}") .top div.logo From 4a94b4729c8b68e80d70c2a8569b6510bfd48c1c Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 8 Dec 2015 17:45:56 +0530 Subject: [PATCH 6/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Removed unused code --- .../external-account/external-link-data.directive.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index e9b9965b2..ec3f5110c 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -102,7 +102,7 @@ .title {{account.data.title}} - div(ng-switch-when="weblink", key="{{account.key}}") + div(ng-switch-when="weblink") p.link-title(data-ellipsis, ng-bind="account.title", ng-hide="account.status === 'PENDING'") p.link-title(ng-show="account.status === 'PENDING'") Loading data. This will take a few minutes. From 1d8d02922a3d85df4ce8ef0deb709707235b4ac8 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Wed, 9 Dec 2015 12:09:20 +0530 Subject: [PATCH 7/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Updated position of trash icon to be such that it does not occupy extra height and we have consistent height of cards for both external accounts and web links. Ref: https://appirio.atlassian.net/browse/SUP-2754?focusedCommentId=20519&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-20519 --- .../external-link-data.directive.jade | 4 ++-- assets/css/directives/external-link-data.scss | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index ec3f5110c..13fc2f5bd 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -1,8 +1,8 @@ .external-link-list div.external-link-tile(ng-repeat="account in linkedAccountsData", ng-class="{'external-link-tile--editable' : editable}") - .ext-link-tile_edit-header(ng-show="editable && account.provider === 'weblink'") - .ext-link-tile_edit-header_delete(ng-click="confirmDeletion(account)", ng-class="{'ext-link-tile_edit-header_delete--disabled': account.deletingAccount || account.status === 'PENDING'}") .top + .ext-link-tile_edit-header(ng-show="editable && account.provider === 'weblink'") + .ext-link-tile_edit-header_delete(ng-click="confirmDeletion(account)", ng-class="{'ext-link-tile_edit-header_delete--disabled': account.deletingAccount || account.status === 'PENDING'}") div.logo i.fa(ng-class="(account|providerData:'className') || 'fa-globe'") h2 {{account|providerData:"displayName"}} diff --git a/assets/css/directives/external-link-data.scss b/assets/css/directives/external-link-data.scss index 016ca83cd..aa8f3423b 100644 --- a/assets/css/directives/external-link-data.scss +++ b/assets/css/directives/external-link-data.scss @@ -6,9 +6,11 @@ external-accounts { } .ext-link-tile_edit-header { - border-bottom: 1px solid $gray-light; display: flex; justify-content: flex-end; + position: absolute; + top: 0px; + right: 0px; &:hover { .show-on-profile_label { @@ -18,7 +20,6 @@ external-accounts { } .ext-link-tile_edit-header_delete { - border-left: 1px solid $gray-light; background-image: url(/images/ico-delete.svg); background-position: center; background-size: 16px 16px; @@ -26,11 +27,18 @@ external-accounts { height: 43px; width: 51px; cursor: pointer; + opacity: 0.7; + + &:hover:not(.ext-link-tile_edit-header_delete--disabled) { + // border-left: 1px solid $gray-light; + // border-bottom: 1px solid $gray-light; + opacity: 1.0; + } } .ext-link-tile_edit-header_delete--disabled { cursor: default; - opacity: 0.5; + opacity: 0.4; } .ext-link-tile_edit-header_show-on-profile { @@ -99,6 +107,7 @@ external-accounts { height: 88px; background-color: $gray-lightest; border-radius: 4px 0 0 4px; + position: relative; i { font-size: 40px; margin-top: 15px; @@ -302,7 +311,7 @@ external-accounts { } .external-link-tile--editable { - height: 285px; + // height: 285px; } } } From c1143617a6ca09d7d6d08fa4d20c15848808e32b Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Wed, 9 Dec 2015 14:35:36 +0530 Subject: [PATCH 8/8] SUP-2754, [Edit Profile] Allow user to hide external links -- Implemented code review suggestion for having separate controller for account deletion logic -- Added link title to the confirmation message with italic(to differentiate it from other text) font style -- Updated unit tests after refactoring --- .../external-link-deletion-confirm.jade | 4 +- .../external-link-deletion.controller.js | 51 +++++ .../external-link-deletion.controller.spec.js | 190 ++++++++++++++++++ .../external-links-data.directive.js | 56 ++---- .../external-links-data.directive.spec.js | 114 +---------- app/index.jade | 1 + .../external-link-deletion-confirm.scss | 4 + 7 files changed, 266 insertions(+), 154 deletions(-) create mode 100644 app/directives/external-account/external-link-deletion.controller.js create mode 100644 app/directives/external-account/external-link-deletion.controller.spec.js diff --git a/app/directives/external-account/external-link-deletion-confirm.jade b/app/directives/external-account/external-link-deletion-confirm.jade index 2667c4b73..58be26244 100644 --- a/app/directives/external-account/external-link-deletion-confirm.jade +++ b/app/directives/external-account/external-link-deletion-confirm.jade @@ -1,8 +1,8 @@ .deletion-confirmation .deletion-confirmation-title Heads Up! - .deletion-confirmation-message Are you sure you want to delete the external link? This action can't be undone later + .deletion-confirmation-message Are you sure you want to delete the external link #[span.deletion-confirmation-account-title "{{vm.account.title}}"]? This action can't be undone later. .deletion-confirmation-buttons .deletion-confirmation-button-yes - button.tc-btn.tc-btn-s.tc-btn-ghost(ng-click="deleteAccount() && closeThisDialog()") Yes, Delete Link + button.tc-btn.tc-btn-s.tc-btn-ghost(ng-click="vm.deleteAccount() && closeThisDialog()") Yes, Delete Link .deletion-confirmation-button-no button.tc-btn.tc-btn-s(ng-click="closeThisDialog()") Cancel \ No newline at end of file diff --git a/app/directives/external-account/external-link-deletion.controller.js b/app/directives/external-account/external-link-deletion.controller.js new file mode 100644 index 000000000..7bef1141a --- /dev/null +++ b/app/directives/external-account/external-link-deletion.controller.js @@ -0,0 +1,51 @@ +(function () { + + angular + .module('tcUIComponents') + .controller('ExternalLinkDeletionController', ExternalLinkDeletionController); + + ExternalLinkDeletionController.$inject = ['ExternalWebLinksService', '$q', '$log', 'toaster', 'ngDialog', 'userHandle', 'account', 'linkedAccountsData']; + + function ExternalLinkDeletionController(ExternalWebLinksService, $q, $log, toaster, ngDialog, userHandle, account, linkedAccountsData) { + var vm = this; + vm.account = account; + $log = $log.getInstance("ExternalLinkDeletionController"); + + vm.deleteAccount = function() { + $log.debug('Deleting Account...'); + if (account && account.deletingAccount) { + $log.debug('Another deletion is already in progress.'); + return; + } + if (account && account.provider === 'weblink') { + account.deletingAccount = true; + $log.debug('Deleting weblink...'); + return ExternalWebLinksService.removeLink(userHandle, account.key).then(function(data) { + account.deletingAccount = false; + $log.debug("Web link removed: " + JSON.stringify(data)); + var toRemove = _.findIndex(linkedAccountsData, function(la) { + return la.provider === 'weblink' && la.key === account.key; + }); + if (toRemove > -1) { + // remove from the linkedAccountsData array + linkedAccountsData.splice(toRemove, 1); + } + toaster.pop('success', "Success", "Your link has been removed."); + }) + .catch(function(resp) { + var msg = resp.msg; + if (resp.status === 'WEBLINK_NOT_EXIST') { + $log.info("Weblink does not exist"); + msg = "Weblink is not linked to your account. If you think this is an error please contact support@topcoder.com."; + } else { + $log.error("Fatal error: _unlink: " + msg); + msg = "Sorry! We are unable to remove your weblink. If problem persists, please contact support@topcoder.com"; + } + + account.deletingAccount = false; + toaster.pop('error', "Whoops!", msg); + }); + } + } + } +})(); \ No newline at end of file diff --git a/app/directives/external-account/external-link-deletion.controller.spec.js b/app/directives/external-account/external-link-deletion.controller.spec.js new file mode 100644 index 000000000..8c57b4bb1 --- /dev/null +++ b/app/directives/external-account/external-link-deletion.controller.spec.js @@ -0,0 +1,190 @@ +/* jshint -W117, -W030 */ +describe('External Link Deletion Controller', function() { + var scope; + var element; + var toasterSvc, extLinkSvc, ngDialogSvc; + var mockLinkedAccounts = [ + { + provider: 'github', + data: { + handle: "github-handle", + followers: 1, + publicRepos: 1 + } + }, + { provider: 'stackoverflow', + data: { + handle: 'so-handle', + reputation: 2, + answers: 2 + } + }, + { + provider: 'behance', + data: { + name: 'behance name', + projectViews: 3, + projectAppreciations: 3 + } + }, + { + provider: 'dribbble', + data: { + handle: 'dribbble-handle', + followers: 4, + likes: 4 + } + }, + { + provider: 'bitbucket', + data: { + username: 'bitbucket-username', + followers: 5, + repositories: 5 + } + }, + { + provider: 'twitter', + data: { + handle: 'twitter-handle', + noOfTweets: 6, + followers: 6 + } + }, + { + provider: 'linkedin', + data: { + status: 'pending' + } + }, + { + provider: 'weblink', + key: 'somekey' + } + ]; + var createController = function(toDelete, linkedAccounts) { + return $controller('ExternalLinkDeletionController', { + ExternalWebLinksService : extLinkSvc, + toaster: toasterSvc, + userHandle: 'test', + account: toDelete, + linkedAccountsData: linkedAccounts + }); + } + + beforeEach(function() { + bard.appModule('topcoder'); + bard.inject(this, '$compile', '$rootScope', 'toaster', 'ExternalWebLinksService', '$q', 'ngDialog', '$controller'); + scope = $rootScope.$new(); + + extLinkSvc = ExternalWebLinksService; + + sinon.stub(extLinkSvc, 'removeLink', function(handle, key) { + var $deferred = $q.defer(); + if (key === 'throwNotExistsError') { + $deferred.reject({ + status: 'WEBLINK_NOT_EXIST', + msg: 'profile not exists' + }); + } else if(key === 'throwFatalError') { + $deferred.reject({ + status: 'FATAL_ERROR', + msg: 'fatal error' + }); + } else { + $deferred.resolve({ + status: 'SUCCESS' + }); + } + return $deferred.promise; + }); + + toasterSvc = toaster; + bard.mockService(toaster, { + pop: $q.when(true), + default: $q.when(true) + }); + + ngDialogSvc = ngDialog; + sinon.stub(ngDialog, 'open', function() { + ngDialog.deferredClose = $q.defer(); + return { closePromise : ngDialog.deferredClose.promise }; + }); + sinon.stub(ngDialog, 'close', function() { + ngDialog.deferredClose.resolve('closing'); + return + }) + }); + + bard.verifyNoOutstandingHttpRequests(); + + describe('Linked external accounts', function() { + var linkedAccounts = null; + var externalLinksData; + + beforeEach(function() { + linkedAccounts = angular.copy(mockLinkedAccounts); + }); + + afterEach(function() { + linkedAccounts = angular.copy(mockLinkedAccounts); + }); + + it('should remove weblink ', function() { + var toDelete = {key: 'somekey', provider: 'weblink'}; + ctrl = createController(toDelete, linkedAccounts); + ctrl.deleteAccount(); + $rootScope.$apply(); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; + expect(linkedAccounts).to.have.length(7); + }); + + it('should show success if controller doesn\'t have weblink but API returns success ', function() { + var toDelete = {key: 'somekey1', provider: 'weblink'}; + ctrl = createController(toDelete, linkedAccounts); + ctrl.deleteAccount(); + $rootScope.$apply(); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; + expect(linkedAccounts).to.have.length(8); + }); + + it('should NOT remove weblink with fatal error ', function() { + var toDelete = {key: 'throwFatalError', provider: 'weblink'}; + ctrl = createController(toDelete, linkedAccounts); + ctrl.deleteAccount(); + $rootScope.$apply(); + expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('Sorry!')).calledOnce; + expect(linkedAccounts).to.have.length(8); + }); + + it('should NOT remove weblink with already removed weblink ', function() { + var toDelete = {key: 'throwNotExistsError', provider: 'weblink'}; + ctrl = createController(toDelete, linkedAccounts); + ctrl.deleteAccount(); + $rootScope.$apply(); + expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('not linked')).calledOnce; + expect(linkedAccounts).to.have.length(8); + }); + + it('should not do any thing when already a deletion is in progress ', function() { + var toDelete = {key: 'somekey', provider: 'weblink', deletingAccount: true}; + ctrl = createController(toDelete, linkedAccounts); + ctrl.deleteAccount(); + $rootScope.$apply(); + expect(extLinkSvc.removeLink).not.to.be.called; + expect(toasterSvc.pop).not.to.be.called; + expect(linkedAccounts).to.have.length(8); + }); + + it('should not do any thing for non weblink provider ', function() { + var toDelete = {key: 'somekey', provider: 'stackoverflow'}; + ctrl = createController(toDelete, linkedAccounts); + ctrl.deleteAccount(); + $rootScope.$apply(); + expect(extLinkSvc.removeLink).not.to.be.called; + expect(toasterSvc.pop).not.to.be.called; + expect(linkedAccounts).to.have.length(8); + }); + + }); +}); diff --git a/app/directives/external-account/external-links-data.directive.js b/app/directives/external-account/external-links-data.directive.js index 8f0a4ac37..e03b3006e 100644 --- a/app/directives/external-account/external-links-data.directive.js +++ b/app/directives/external-account/external-links-data.directive.js @@ -22,59 +22,29 @@ function($log, $scope, ExternalWebLinksService, toaster, ngDialog) { $log = $log.getInstance("ExternalLinksDataCtrl"); - $scope.toDelete = null; $scope.deletionDialog = null; $scope.confirmDeletion = function(account) { - $scope.toDelete = account; $scope.deletionDialog = ngDialog.open({ className: 'ngdialog-theme-default tc-dialog', template: 'directives/external-account/external-link-deletion-confirm.html', - scope: $scope + controller: 'ExternalLinkDeletionController', + controllerAs: 'vm', + resolve: { + userHandle: function() { + return $scope.userHandle; + }, + account: function() { + return account; + }, + linkedAccountsData: function() { + return $scope.linkedAccountsData; + } + } }).closePromise.then(function (data) { $log.debug('Closing deletion confirmation dialog.'); - $scope.toDelete = null; }); } - - $scope.deleteAccount = function() { - $log.debug('Deleting Account...'); - var account = $scope.toDelete; - if (account && account.deletingAccount) { - $log.debug('Another deletion is already in progress.'); - return; - } - if (account && account.provider === 'weblink') { - account.deletingAccount = true; - $log.debug('Deleting weblink...'); - return ExternalWebLinksService.removeLink($scope.userHandle, account.key).then(function(data) { - $scope.deletingAccount = false; - $log.debug("Web link removed: " + JSON.stringify(data)); - var toRemove = _.findIndex($scope.linkedAccountsData, function(la) { - return la.provider === 'weblink' && la.key === account.key; - }); - if (toRemove > -1) { - // remove from the linkedAccountsData array - $scope.linkedAccountsData.splice(toRemove, 1); - } - account.deletingAccount = false; - toaster.pop('success', "Success", "Your link has been removed."); - }) - .catch(function(resp) { - var msg = resp.msg; - if (resp.status === 'WEBLINK_NOT_EXIST') { - $log.info("Weblink does not exist"); - msg = "Weblink is not linked to your account. If you think this is an error please contact support@topcoder.com."; - } else { - $log.error("Fatal error: _unlink: " + msg); - msg = "Sorry! We are unable to remove your weblink. If problem persists, please contact support@topcoder.com"; - } - - account.deletingAccount = false; - toaster.pop('error', "Whoops!", msg); - }); - } - } } ] }; diff --git a/app/directives/external-account/external-links-data.directive.spec.js b/app/directives/external-account/external-links-data.directive.spec.js index fd075f9bb..76592c65e 100644 --- a/app/directives/external-account/external-links-data.directive.spec.js +++ b/app/directives/external-account/external-links-data.directive.spec.js @@ -1,8 +1,8 @@ /* jshint -W117, -W030 */ -xdescribe('External Links Data Directive', function() { +describe('External Links Data Directive', function() { var scope; var element; - var toasterSvc, extLinkSvc, ngDialogSvc; + var ngDialogSvc; var mockLinkedAccounts = [ { provider: 'github', @@ -65,37 +65,9 @@ xdescribe('External Links Data Directive', function() { beforeEach(function() { bard.appModule('topcoder'); - bard.inject(this, '$compile', '$rootScope', 'toaster', 'ExternalWebLinksService', '$q', 'ngDialog'); + bard.inject(this, '$compile', '$rootScope', '$q', 'ngDialog'); scope = $rootScope.$new(); - extLinkSvc = ExternalWebLinksService; - - sinon.stub(extLinkSvc, 'removeLink', function(handle, key) { - var $deferred = $q.defer(); - if (key === 'throwNotExistsError') { - $deferred.reject({ - status: 'WEBLINK_NOT_EXIST', - msg: 'profile not exists' - }); - } else if(key === 'throwFatalError') { - $deferred.reject({ - status: 'FATAL_ERROR', - msg: 'fatal error' - }); - } else { - $deferred.resolve({ - status: 'SUCCESS' - }); - } - return $deferred.promise; - }); - - toasterSvc = toaster; - bard.mockService(toaster, { - pop: $q.when(true), - default: $q.when(true) - }); - ngDialogSvc = ngDialog; sinon.stub(ngDialog, 'open', function() { ngDialog.deferredClose = $q.defer(); @@ -131,74 +103,7 @@ xdescribe('External Links Data Directive', function() { expect(element.isolateScope().linkedAccountsData).to.have.length(8); }); - it('should remove weblink ', function() { - element.isolateScope().toDelete = {key: 'somekey', provider: 'weblink'}; - element.isolateScope().deleteAccount(); - scope.$digest(); - expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; - expect(element.isolateScope().linkedAccountsData).to.have.length(7); - }); - - it('should show success if controller doesn\'t have weblink but API returns success ', function() { - element.isolateScope().toDelete = {key: 'somekey1', provider: 'weblink'}; - element.isolateScope().deleteAccount(); - scope.$digest(); - expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; - expect(element.isolateScope().linkedAccountsData).to.have.length(8); - }); - - it('should NOT remove weblink with fatal error ', function() { - element.isolateScope().toDelete = {key: 'throwFatalError', provider: 'weblink'}; - element.isolateScope().deleteAccount(); - scope.$digest(); - expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('Sorry!')).calledOnce; - expect(element.isolateScope().linkedAccountsData).to.have.length(8); - }); - - it('should NOT remove weblink with already removed weblink ', function() { - element.isolateScope().toDelete = {key: 'throwNotExistsError', provider: 'weblink'}; - element.isolateScope().deleteAccount(); - scope.$digest(); - expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('not linked')).calledOnce; - expect(element.isolateScope().linkedAccountsData).to.have.length(8); - }); - - it('should not do any thing when already a deletion is in progress ', function() { - element.isolateScope().toDelete = {key: 'somekey', provider: 'weblink', deletingAccount: true}; - element.isolateScope().deleteAccount(); - scope.$digest(); - expect(extLinkSvc.removeLink).not.to.be.called; - expect(toasterSvc.pop).not.to.be.called; - expect(element.isolateScope().linkedAccountsData).to.have.length(8); - }); - - it('should not do any thing for non weblink provider ', function() { - element.isolateScope().toDelete = {key: 'somekey', provider: 'stackoverflow'}; - element.isolateScope().deleteAccount(); - scope.$digest(); - expect(extLinkSvc.removeLink).not.to.be.called; - expect(toasterSvc.pop).not.to.be.called; - expect(element.isolateScope().linkedAccountsData).to.have.length(8); - }); - it('should mark the account for deletion and open the popup ', function() { - var account = {key: 'somekey', provider: 'stackoverflow'}; - element.isolateScope().confirmDeletion(account); - scope.$digest(); - // just to cross check that it does not call removal service directly - expect(extLinkSvc.removeLink).not.to.be.called; - // should open the popup - expect(ngDialogSvc.open).to.be.called; - // should not remove anythign from the array of accounts/links - expect(element.isolateScope().linkedAccountsData).to.have.length(8); - // $scope.toDelete should point to account passed to the confirmDeletion method - expect(element.isolateScope().toDelete).to.exist; - expect(element.isolateScope().toDelete.key).to.exist.to.equal(account.key); - expect(element.isolateScope().toDelete.provider).to.exist.to.equal(account.provider); - ngDialogSvc.close(); - }); - - it('should call closePromise after popup closing ', function() { var account = {key: 'somekey', provider: 'stackoverflow'}; element.isolateScope().confirmDeletion(account); scope.$digest(); @@ -206,17 +111,8 @@ xdescribe('External Links Data Directive', function() { expect(ngDialogSvc.open).to.be.called; // should not remove anythign from the array of accounts/links expect(element.isolateScope().linkedAccountsData).to.have.length(8); - // $scope.toDelete should point to account passed to the confirmDeletion method - expect(element.isolateScope().toDelete).to.exist; - expect(element.isolateScope().toDelete.key).to.exist.to.equal(account.key); - expect(element.isolateScope().toDelete.provider).to.exist.to.equal(account.provider); - // adds resolve listener to the closePromise of ngDialog to verify that closePromise - // has been called after closing the ngDialog - ngDialogSvc.deferredClose.promise.then(function() { - // should reset scope.toDelete to null - expect(element.isolateScope().toDelete).to.null; - }); - // close dialog and resolve closePromise + // $scope.deletionDialog should exist + expect(element.isolateScope().deletionDialog).to.exist; ngDialogSvc.close(); }); diff --git a/app/index.jade b/app/index.jade index e815fc3bf..3bf24f775 100644 --- a/app/index.jade +++ b/app/index.jade @@ -194,6 +194,7 @@ html script(src="directives/distribution-graph/distribution-graph.directive.js") script(src="directives/empty-state-placeholder/empty-state-placeholder.directive.js") script(src="directives/external-account/external-account.directive.js") + script(src="directives/external-account/external-link-deletion.controller.js") script(src="directives/external-account/external-links-data.directive.js") script(src="directives/external-account/external-web-links.directive.js") script(src="directives/focus-on.directive.js") diff --git a/assets/css/directives/external-link-deletion-confirm.scss b/assets/css/directives/external-link-deletion-confirm.scss index c2c1377cc..77c8a1051 100644 --- a/assets/css/directives/external-link-deletion-confirm.scss +++ b/assets/css/directives/external-link-deletion-confirm.scss @@ -28,6 +28,10 @@ .deletion-confirmation-message { @include merriweather-sans-regular;//TODO use sansbook font + + .deletion-confirmation-account-title { + font-style: italic; + } } .deletion-confirmation-buttons {