Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.

Commit 5d3b509

Browse files
author
vikasrohit
committed
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
1 parent 4b96b72 commit 5d3b509

File tree

8 files changed

+276
-65
lines changed

8 files changed

+276
-65
lines changed

app/directives/external-account/external-link-data.directive.jade

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
.external-link-list
22
div.external-link-tile(ng-repeat="account in linkedAccountsData", ng-class="{'external-link-tile--editable' : editable}")
33
.ext-link-tile_edit-header(ng-show="editable")
4-
.ext-link-tile_edit-header_show-on-profile
5-
span.show-on-profile_label Show on Profile
6-
onoff-switch(model="account.hide", unique-id="account.provider + '_' + (account.key || account.data.handle)")
4+
.ext-link-tile_edit-header_delete(ng-click="deleteAccount(account)", ng-class="{'ext-link-tile_edit-header_delete--deleting': deletingAccount}")
75
.top
86
div.logo
97
i.fa(ng-class="(account|providerData:'className') || 'fa-globe'")

app/directives/external-account/external-links-data.directive.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,51 @@
1515
templateUrl: 'directives/external-account/external-link-data.directive.html',
1616
scope: {
1717
linkedAccountsData: '=',
18-
editable: '='
19-
}
18+
editable: '=',
19+
userHandle: '@'
20+
},
21+
controller: ['$log', '$scope', 'ExternalWebLinksService', 'toaster',
22+
function($log, $scope, ExternalWebLinksService, toaster) {
23+
24+
$log = $log.getInstance("ExternalLinksDataCtrl");
25+
$scope.deletingAccount = false;
26+
27+
$scope.deleteAccount = function(account) {
28+
$log.debug('Deleting Account...');
29+
if ($scope.deletingAccount) {
30+
$log.debug('Another deletion is already in progress.');
31+
return;
32+
}
33+
if (account && account.provider === 'weblink') {
34+
$log.debug('Deleting weblink...');
35+
$scope.deletingAccount = true;
36+
ExternalWebLinksService.removeLink($scope.userHandle, account.key).then(function(data) {
37+
$scope.deletingAccount = false;
38+
$log.debug("Web link removed: " + JSON.stringify(data));
39+
var toRemove = _.findIndex($scope.linkedAccountsData, function(la) {
40+
return la.provider === 'weblink' && la.key === account.key;
41+
});
42+
if (toRemove > -1) {
43+
// remove from the linkedAccountsData array
44+
$scope.linkedAccountsData.splice(toRemove, 1);
45+
}
46+
toaster.pop('success', "Success", "Your link has been added. Data from your link will be visible on your profile shortly.");
47+
})
48+
.catch(function(resp) {
49+
var msg = resp.msg;
50+
if (resp.status === 'WEBLINK_NOT_EXIST') {
51+
$log.info("Weblink does not exist");
52+
msg = "Weblink is not linked to your account. If you think this is an error please contact <a href=\"mailTo:[email protected]\">[email protected]</a>.";
53+
} else {
54+
$log.error("Fatal error: _unlink: " + msg);
55+
msg = "Sorry! We are unable to remove your weblink. If problem persists, please contact <a href=\"mailTo:[email protected]\">[email protected]</a>";
56+
}
57+
toaster.pop('error', "Whoops!", msg);
58+
});
59+
}
60+
}
61+
}
62+
]
2063
};
2164
return directive;
2265
}

app/directives/external-account/external-links-data.directive.spec.js

Lines changed: 142 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,84 +2,169 @@
22
describe('External Links Data Directive', function() {
33
var scope;
44
var element;
5+
var toasterSvc, extLinkSvc;
6+
var mockLinkedAccounts = [
7+
{
8+
provider: 'github',
9+
data: {
10+
handle: "github-handle",
11+
followers: 1,
12+
publicRepos: 1
13+
}
14+
},
15+
{ provider: 'stackoverflow',
16+
data: {
17+
handle: 'so-handle',
18+
reputation: 2,
19+
answers: 2
20+
}
21+
},
22+
{
23+
provider: 'behance',
24+
data: {
25+
name: 'behance name',
26+
projectViews: 3,
27+
projectAppreciations: 3
28+
}
29+
},
30+
{
31+
provider: 'dribbble',
32+
data: {
33+
handle: 'dribbble-handle',
34+
followers: 4,
35+
likes: 4
36+
}
37+
},
38+
{
39+
provider: 'bitbucket',
40+
data: {
41+
username: 'bitbucket-username',
42+
followers: 5,
43+
repositories: 5
44+
}
45+
},
46+
{
47+
provider: 'twitter',
48+
data: {
49+
handle: 'twitter-handle',
50+
noOfTweets: 6,
51+
followers: 6
52+
}
53+
},
54+
{
55+
provider: 'linkedin',
56+
data: {
57+
status: 'pending'
58+
}
59+
},
60+
{
61+
provider: 'weblink',
62+
key: 'somekey'
63+
}
64+
];
565

666
beforeEach(function() {
767
bard.appModule('topcoder');
8-
bard.inject(this, '$compile', '$rootScope');
68+
bard.inject(this, '$compile', '$rootScope', 'toaster', 'ExternalWebLinksService', '$q');
969
scope = $rootScope.$new();
70+
71+
extLinkSvc = ExternalWebLinksService;
72+
73+
sinon.stub(extLinkSvc, 'removeLink', function(handle, key) {
74+
var $deferred = $q.defer();
75+
if (key === 'throwNotExistsError') {
76+
$deferred.reject({
77+
status: 'WEBLINK_NOT_EXIST',
78+
msg: 'profile not exists'
79+
});
80+
} else if(key === 'throwFatalError') {
81+
$deferred.reject({
82+
status: 'FATAL_ERROR',
83+
msg: 'fatal error'
84+
});
85+
} else {
86+
$deferred.resolve({
87+
status: 'SUCCESS'
88+
});
89+
}
90+
return $deferred.promise;
91+
});
92+
93+
toasterSvc = toaster;
94+
bard.mockService(toaster, {
95+
pop: $q.when(true),
96+
default: $q.when(true)
97+
});
1098
});
1199

12100
bard.verifyNoOutstandingHttpRequests();
13101

14102
describe('Linked external accounts', function() {
15-
var linkedAccounts = [
16-
{
17-
provider: 'github',
18-
data: {
19-
handle: "github-handle",
20-
followers: 1,
21-
publicRepos: 1
22-
}
23-
},
24-
{ provider: 'stackoverflow',
25-
data: {
26-
handle: 'so-handle',
27-
reputation: 2,
28-
answers: 2
29-
}
30-
},
31-
{
32-
provider: 'behance',
33-
data: {
34-
name: 'behance name',
35-
projectViews: 3,
36-
projectAppreciations: 3
37-
}
38-
},
39-
{
40-
provider: 'dribbble',
41-
data: {
42-
handle: 'dribbble-handle',
43-
followers: 4,
44-
likes: 4
45-
}
46-
},
47-
{
48-
provider: 'bitbucket',
49-
data: {
50-
username: 'bitbucket-username',
51-
followers: 5,
52-
repositories: 5
53-
}
54-
},
55-
{
56-
provider: 'twitter',
57-
data: {
58-
handle: 'twitter-handle',
59-
noOfTweets: 6,
60-
followers: 6
61-
}
62-
},
63-
{
64-
provider: 'linkedin',
65-
data: {
66-
status: 'pending'
67-
}
68-
}
69-
];
103+
var linkedAccounts = null;
70104
var externalLinksData;
71105

72106
beforeEach(function() {
107+
linkedAccounts = angular.copy(mockLinkedAccounts);
73108
scope.linkedAccounts = linkedAccounts;
74-
element = angular.element('<external-links-data linked-accounts-data="linkedAccounts"></external-links-data>)');
109+
element = angular.element('<external-links-data linked-accounts-data="linkedAccounts" user-handle="test"></external-links-data>)');
75110
externalLinksData = $compile(element)(scope);
76111
scope.$digest();
77112
});
78113

114+
afterEach(function() {
115+
linkedAccounts = angular.copy(mockLinkedAccounts);
116+
scope.linkedAccounts = linkedAccounts;
117+
});
118+
79119
it('should have added linkedAccounts to scope', function() {
80120
expect(element.isolateScope().linkedAccountsData).to.exist;
121+
expect(element.isolateScope().linkedAccountsData).to.have.length(8);
122+
});
123+
124+
it('should remove weblink ', function() {
125+
element.isolateScope().deleteAccount({key: 'somekey', provider: 'weblink'});
126+
scope.$digest();
127+
expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce;
81128
expect(element.isolateScope().linkedAccountsData).to.have.length(7);
82129
});
83130

131+
it('should show success if controller doesn\'t have weblink but API returns success ', function() {
132+
element.isolateScope().deleteAccount({key: 'somekey1', provider: 'weblink'});
133+
scope.$digest();
134+
expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce;
135+
expect(element.isolateScope().linkedAccountsData).to.have.length(8);
136+
});
137+
138+
it('should NOT remove weblink with fatal error ', function() {
139+
element.isolateScope().deleteAccount({key: 'throwFatalError', provider: 'weblink'});
140+
scope.$digest();
141+
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('Sorry!')).calledOnce;
142+
expect(element.isolateScope().linkedAccountsData).to.have.length(8);
143+
});
144+
145+
it('should NOT remove weblink with already removed weblink ', function() {
146+
element.isolateScope().deleteAccount({key: 'throwNotExistsError', provider: 'weblink'});
147+
scope.$digest();
148+
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('not linked')).calledOnce;
149+
expect(element.isolateScope().linkedAccountsData).to.have.length(8);
150+
});
151+
152+
it('should not do any thing when already a deletion is in progress ', function() {
153+
element.isolateScope().deletingAccount = true;
154+
element.isolateScope().deleteAccount({key: 'somekey', provider: 'weblink'});
155+
scope.$digest();
156+
expect(extLinkSvc.removeLink).not.to.be.called;
157+
expect(toasterSvc.pop).not.to.be.called;
158+
expect(element.isolateScope().linkedAccountsData).to.have.length(8);
159+
});
160+
161+
it('should not do any thing for non weblink provider ', function() {
162+
element.isolateScope().deleteAccount({key: 'somekey', provider: 'stackoverflow'});
163+
scope.$digest();
164+
expect(extLinkSvc.removeLink).not.to.be.called;
165+
expect(toasterSvc.pop).not.to.be.called;
166+
expect(element.isolateScope().linkedAccountsData).to.have.length(8);
167+
});
168+
84169
});
85170
});

app/services/externalLinks.service.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,23 @@
6767
}
6868

6969
function removeLink(userHandle, key) {
70-
return memberApi.one('members', userHandle).one('externalLinks', key).remove();
70+
return $q(function($resolve, $reject) {
71+
return memberApi.one('members', userHandle).one('externalLinks', key).remove()
72+
.then(function(resp) {
73+
$resolve(resp);
74+
})
75+
.catch(function(resp) {
76+
var errorStatus = "FATAL_ERROR";
77+
$log.error("Error removing weblink: " + resp.data.result.content);
78+
if (resp.data.result && resp.data.result.status === 400) {
79+
errorStatus = "WEBLINK_NOT_EXIST";
80+
}
81+
$reject({
82+
status: errorStatus,
83+
msg: resp.data.result.content
84+
});
85+
});
86+
});
7187
}
7288

7389
}

app/services/externalLinks.service.spec.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,24 @@ describe('ExternalWebLinks service', function() {
6464
$httpBackend.flush();
6565
});
6666

67+
it('should fail, with fatal error, in adding link', function() {
68+
var errorMessage = "endpoint not found";
69+
linksPost.respond(404, {result: { status: 404, content: errorMessage } });
70+
// call addLink method with valid args
71+
service.addLink('test1', "http://google.com").then(function(data) {
72+
sinon.assert.fail('should not be called');
73+
}).catch(function(error) {
74+
expect(error).to.be.defined;
75+
expect(error.status).to.exist.to.equal('FATAL_ERROR');
76+
expect(error.msg).to.exist.to.equal(errorMessage);
77+
});
78+
$httpBackend.flush();
79+
});
80+
6781
it('should fail with already existing link', function() {
6882
var errorMessage = "web link exists";
6983
linksPost.respond(400, {result: { status: 400, content: errorMessage } });
70-
// call linkExternalAccount method, having user service throw already exist
84+
// call addLink method, having user service throw already exist
7185
service.addLink('test1', "http://google.com").then(function(data) {
7286
sinon.assert.fail('should not be called');
7387
}, function(error) {
@@ -87,4 +101,32 @@ describe('ExternalWebLinks service', function() {
87101
$httpBackend.flush();
88102
});
89103

104+
it('should fail with non existing link', function() {
105+
var errorMessage = "web link does not exists";
106+
linksDelete.respond(400, {result: { status: 400, content: errorMessage } });
107+
// call removeLink method
108+
service.removeLink('test1', "testkey").then(function(data) {
109+
sinon.assert.fail('should not be called');
110+
}, function(error) {
111+
expect(error).to.be.defined;
112+
expect(error.status).to.exist.to.equal('WEBLINK_NOT_EXIST');
113+
expect(error.msg).to.exist.to.equal(errorMessage);
114+
});
115+
$httpBackend.flush();
116+
});
117+
118+
it('should fail, with fatal error, in removing link', function() {
119+
var errorMessage = "endpoint not found";
120+
linksDelete.respond(404, {result: { status: 404, content: errorMessage } });
121+
// call removeLink method with valid args
122+
service.removeLink('test1', "testkey").then(function(data) {
123+
sinon.assert.fail('should not be called');
124+
}).catch(function(error) {
125+
expect(error).to.be.defined;
126+
expect(error.status).to.exist.to.equal('FATAL_ERROR');
127+
expect(error.msg).to.exist.to.equal(errorMessage);
128+
});
129+
$httpBackend.flush();
130+
});
131+
90132
});

app/settings/edit-profile/edit-profile.jade

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,4 @@
108108
.field-label Linked Accounts
109109

110110
.existing-links
111-
external-links-data(linked-accounts-data="vm.linkedExternalAccountsData", editable="true")
111+
external-links-data(linked-accounts-data="vm.linkedExternalAccountsData", editable="true", user-handle="{{vm.userData.handle}}")

0 commit comments

Comments
 (0)