From 4e4fa385e11bc11c91c423ced0bbcba435db2047 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 5 Oct 2015 17:22:06 -0700 Subject: [PATCH 01/12] SUP-1611, Settings: Support adding an external web link. -- Added option to add web link in UI. Seems like API don't have support for adding web links as of now. --- .../external-account.directive.js | 57 +++++++++++++++- .../external-web-link.directive.jade | 14 ++++ app/index.jade | 1 + app/services/externalAccounts.service.js | 16 ++++- app/services/user.service.js | 7 +- app/settings/edit-profile/edit-profile.jade | 5 ++ assets/css/directives/external-web-link.scss | 68 +++++++++++++++++++ 7 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 app/directives/external-account/external-web-link.directive.jade create mode 100644 assets/css/directives/external-web-link.scss diff --git a/app/directives/external-account/external-account.directive.js b/app/directives/external-account/external-account.directive.js index 034b20ac5..ae22ab929 100644 --- a/app/directives/external-account/external-account.directive.js +++ b/app/directives/external-account/external-account.directive.js @@ -9,7 +9,7 @@ { provider: "github", className: "fa-github", displayName: "Github", disabled: false, order: 1, colorClass: 'el-github'}, { provider: "bitbucket", className: "fa-bitbucket", displayName: "Bitbucket", disabled: true, order: 7, colorClass: 'el-bitbucket'}, { provider: "twitter", className: "fa-twitter", displayName: "Twitter", disabled: true, order: 4, colorClass: 'el-twitter'}, - { provider: "weblinks", className: "fa-globe", displayName: "Web Links", disabled: true, order: 8, colorClass: 'el-weblinks'} + //{ provider: "weblinks", className: "fa-globe", displayName: "Web Links", disabled: true, order: 8, colorClass: 'el-weblinks'} // TODO add more ]; @@ -26,6 +26,7 @@ function($log, $scope, ExternalAccountService, toaster) { $log = $log.getInstance("ExtAccountDirectiveCtrl") $scope.accountList = _.clone(_supportedAccounts, true); + $scope.$watch('linkedAccounts', function(newValue, oldValue) { for (var i=0;i<$scope.accountList.length;i++) { $scope.accountList[i].linked = !!_.find(newValue, function(a) { @@ -114,6 +115,60 @@ ] } }) + .directive('externalWebLink', function() { + return { + restrict: 'E', + templateUrl: 'directives/external-account/external-web-link.directive.html', + scope: { + linkedAccounts: '=', + userData: "=" + }, + controller: ['$log', '$scope', 'ExternalAccountService', + function($log, $scope, ExternalAccountService) { + $log = $log.getInstance('ExternalWebLinkDirective'); + $scope.addingWebLink = false; + $scope.errorMessage = null; + + $log.debug("userData: " + $scope.userData.handle); + + $scope.addWebLink = function() { + $log.debug("URL: " + $scope.url); + $scope.addingWebLink = true; + $scope.errorMessage = null; + ExternalAccountService.addWebLink($scope.userData.userId, $scope.userData.handle, $scope.url) + .then(function(resp) { + $scope.addingWebLink = false; + $log.debug("Web link added: " + JSON.stringify(resp)); + $scope.linkedAccounts.push(resp.profile); + toaster.pop('success', "Success", + String.supplant( + "Your link has been added. Data from your link will be visible on your profile shortly.", + {provider: extAccountProvider} + ) + ); + }) + .catch(function(resp) { + $scope.addingWebLink = false; + $log.debug(JSON.stringify(resp)); + if (resp.status === 'SOCIAL_PROFILE_ALREADY_EXISTS') { + $log.info("Social profile already linked to another account"); + toaster.pop('error', "Whoops!", + String.supplant( + "This {provider} account is linked to another account. \ + If you think this is an error please contact support@apprio.com.", + {provider: extAccountProvider } + ) + ); + } else { + $log.info("Server error:" + resp.content); + $scope.errorMessage = "Sorry, we are unable add web link. If problem persist please contact support@topcoder.com"; + } + }); + }; + } + ] + } + }) .filter('providerData', function() { return function(input, field) { return _.result(_.find(_supportedAccounts, function(s) { diff --git a/app/directives/external-account/external-web-link.directive.jade b/app/directives/external-account/external-web-link.directive.jade new file mode 100644 index 000000000..ec0402474 --- /dev/null +++ b/app/directives/external-account/external-web-link.directive.jade @@ -0,0 +1,14 @@ +.web-link + form(name="addWebLinkFrm", ng-submit="addWebLinkFrm.$valid && addWebLink()", autocomplete="off") + .validation-bar.url(ng-class="{ 'error-bar': (addWebLinkFrm.url.$dirty && addWebLinkFrm.url.$invalid) }") + input.form-field.url(name="url", type="url", ng-model="url", placeholder="http://www.youlink.com", required) + + .form-input-error(ng-show="addWebLinkFrm.url.$dirty && addWebLinkFrm.url.$invalid") + p(ng-show="addWebLinkFrm.url.$error.required") This is a required field. + + p(ng-show="addWebLinkFrm.url.$error.url") Please enter a vadlid URL + + button(type="submit", tc-busy-button, tc-busy-when="addingWebLink", tc-busy-message="Adding", ng-disabled="addWebLinkFrm.$invalid || addWebLinkFrm.$pristine", ng-class="{'enabled-button': addWebLinkFrm.$valid, 'disabled': addWebLinkFrm.$pristine || addWebLinkFrm.$invalid, 'busy' : addingWebLink}") Add + + .form-errors(ng-show="errorMessage") + p.form-error {{errorMessage}} \ No newline at end of file diff --git a/app/index.jade b/app/index.jade index f4bac77b4..c496aa785 100644 --- a/app/index.jade +++ b/app/index.jade @@ -65,6 +65,7 @@ html link(rel="stylesheet", href="assets/css/directives/ios-card.css") link(rel="stylesheet", href="assets/css/directives/input-sticky-placeholder.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-data.css") link(rel="stylesheet", href="assets/css/directives/external-account.css") link(rel="stylesheet", href="assets/css/directives/distribution-graph.css") diff --git a/app/services/externalAccounts.service.js b/app/services/externalAccounts.service.js index 927fc6951..7c662d960 100644 --- a/app/services/externalAccounts.service.js +++ b/app/services/externalAccounts.service.js @@ -15,7 +15,8 @@ getLinkedExternalAccounts: getLinkedExternalAccounts, getLinkedExternalLinksData: getLinkedExternalLinksData, linkExternalAccount: linkExternalAccount, - unlinkExternalAccount: unlinkExternalAccount + unlinkExternalAccount: unlinkExternalAccount, + addWebLink : addWebLink }; return service; @@ -45,6 +46,19 @@ throw new Error("not implemented"); } + function addWebLink(userId, userHandle, url) { + var provider = "url::" + url; + var postData = { + userId: userId, + providerType: provider, + context: { + handle: userHandle + } + }; + // delegates to user service + return UserService.addSocialProfile(userId, postData); + } + function linkExternalAccount(provider, callbackUrl) { return $q(function(resolve, reject) { // supported backends diff --git a/app/services/user.service.js b/app/services/user.service.js index ecab69e54..56cccb983 100644 --- a/app/services/user.service.js +++ b/app/services/user.service.js @@ -24,7 +24,8 @@ resetPassword: resetPassword, updatePassword: updatePassword, getUserProfile: getUserProfile, - getV2UserProfile: getV2UserProfile + getV2UserProfile: getV2UserProfile, + addSocialProfile: addSocialProfile }; return service; @@ -99,6 +100,10 @@ return api.one('users', userId).get(queryParams); } + function addSocialProfile(userId, profileData) { + return api.one('users', userId).customPOST(profileData, "profiles", {}, {}); + } + /** * Temporary end point for getting member's badges/achievements. This endpoint * should be removed once we have it in v3. diff --git a/app/settings/edit-profile/edit-profile.jade b/app/settings/edit-profile/edit-profile.jade index 76d707759..ef8e841a2 100644 --- a/app/settings/edit-profile/edit-profile.jade +++ b/app/settings/edit-profile/edit-profile.jade @@ -84,6 +84,11 @@ .description Show off your work and experience outside of Topcoder. Connect accounts from popular services and networks or add a link to any site. .section-fields + .field-label Add a web link + + .web-links + external-web-link(linked-accounts="vm.linkedExternalAccounts", user-data="vm.userData", read-only="false") + .field-label Link Your Accounts .external-links diff --git a/assets/css/directives/external-web-link.scss b/assets/css/directives/external-web-link.scss new file mode 100644 index 000000000..ca355e62e --- /dev/null +++ b/assets/css/directives/external-web-link.scss @@ -0,0 +1,68 @@ +@import '../partials/combined'; + +external-web-link { + .web-link { + margin: 6px 0 31px 0; + + .form-errors { + position: initial; + width: 100%; + } + + form { + display: flex; + flex-flow: row wrap; + + + .form-label { + @include sofia-pro-regular; + font-size: 12px; + color: black; + text-transform: uppercase; + margin-bottom: 5px; + margin-top: 5px; + } + .form-field { + @include form-field; + @include ui-form-placeholder; + &:disabled { + color: #B7B7B7; + } + } + .form-field-focused { + @include form-field-focused; + } + + .validation-bar.url { + flex: 1; + + &:before { + height: 40px; + } + } + + input.url { + width: 100%; + margin: 0px; + } + + button { + @include button-2-m; + font-size: 14px; + line-height: 16px; + height: 40px; + width: 60px; + margin: 0 0 0 10px; + text-transform: uppercase; + + &.enabled-button { + @include ui-enabled-button; + } + + &.busy { + width: 94px; + } + } + } + } +} \ No newline at end of file From c84af5901858cf3cd3f56cafa648ffcc29c66577 Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Mon, 26 Oct 2015 08:16:39 -0700 Subject: [PATCH 02/12] major refactoring, moving data processing to services --- .../external-account.directive.js | 140 ++------- .../external-account.directive.spec.js | 119 ++------ .../external-link-data.directive.jade | 6 +- .../external-links-data.directive.js | 22 ++ .../external-links-data.directive.spec.js | 85 ++++++ .../external-web-link.directive.jade | 15 +- .../external-web-links.directive.js | 69 +++++ app/filters/external-link-color.filter.js | 2 +- app/index.jade | 3 + .../subtrack-stats/subtrack-stats.jade | 2 +- app/profile/about/about.controller.js | 48 +-- app/profile/about/about.jade | 5 +- app/services/externalAccounts.service.js | 91 ++++-- app/services/externalAccounts.service.spec.js | 76 +++++ app/services/externalLinks.service.js | 50 ++++ app/services/externalLinks.service.spec.js | 44 +++ app/services/user.service.js | 2 +- .../edit-profile/edit-profile.controller.js | 37 ++- app/settings/edit-profile/edit-profile.jade | 6 +- app/specs.html | 23 +- assets/css/directives/external-web-link.scss | 32 +- tests/test-helpers/mock-data.js | 275 ++++++++++-------- 22 files changed, 707 insertions(+), 445 deletions(-) create mode 100644 app/directives/external-account/external-links-data.directive.js create mode 100644 app/directives/external-account/external-links-data.directive.spec.js create mode 100644 app/directives/external-account/external-web-links.directive.js create mode 100644 app/services/externalAccounts.service.spec.js create mode 100644 app/services/externalLinks.service.js create mode 100644 app/services/externalLinks.service.spec.js diff --git a/app/directives/external-account/external-account.directive.js b/app/directives/external-account/external-account.directive.js index 08737837b..7fae68510 100644 --- a/app/directives/external-account/external-account.directive.js +++ b/app/directives/external-account/external-account.directive.js @@ -1,15 +1,14 @@ (function() { 'use strict'; var _supportedAccounts = [ - { provider: "dribbble", className: "fa-dribbble", displayName: "Dribbble", disabled: false, order: 6, colorClass: 'el-dribble'}, + { provider: "dribbble", className: "fa-dribbble", displayName: "Dribbble", disabled: false, order: 6, colorClass: 'el-dribbble'}, { provider: "linkedin", className: "fa-linkedin", displayName: "LinkedIn", disabled: true, order: 5, colorClass: 'el-linkedin'}, { provider: "stackoverflow", className: "fa-stack-overflow", displayName: "Stack Overflow", disabled: false, order: 3, colorClass: 'el-stackoverflow'}, { provider: "behance", className: "fa-behance", displayName: "Behance", disabled: true, order: 2, colorClass: 'el-behance'}, - // { provider: "google-oauth2", className: "fa-google-plus", displayName: "Google+", disabled: true, order: }, colorClass: 'el-dribble', { provider: "github", className: "fa-github", displayName: "Github", disabled: false, order: 1, colorClass: 'el-github'}, { provider: "bitbucket", className: "fa-bitbucket", displayName: "Bitbucket", disabled: false, order: 7, colorClass: 'el-bitbucket'}, { provider: "twitter", className: "fa-twitter", displayName: "Twitter", disabled: true, order: 4, colorClass: 'el-twitter'}, - //{ provider: "weblinks", className: "fa-globe", displayName: "Web Links", disabled: true, order: 8, colorClass: 'el-weblinks'} + { provider: "weblink", className: "fa-globe", displayName: "Web Links", disabled: true, order: -1, colorClass: 'el-weblinks'} // TODO add more ]; @@ -20,28 +19,31 @@ templateUrl: 'directives/external-account/external-account.directive.html', scope: { linkedAccounts: '=', - linksData: '=', + // linksData: '=', readOnly: '=' }, controller: ['$log', '$scope', 'ExternalAccountService', 'toaster', function($log, $scope, ExternalAccountService, toaster) { + $log = $log.getInstance("ExtAccountDirectiveCtrl") - $scope.accountList = _.clone(_supportedAccounts, true); + + var _accountList = _.clone(_supportedAccounts, true); + _.remove(_accountList, function(al) { return al.order < 0}); $scope.$watchCollection('linkedAccounts', function(newValue, oldValue) { - for (var i=0;i<$scope.accountList.length;i++) { - var _idx = _.findIndex(newValue, function(a) { - return $scope.accountList[i].provider === a.providerType; - }); - if (_idx == -1) { - $scope.accountList[i].status = 'unlinked'; - } else { - // check if data - if ($scope.linksData[$scope.accountList[i].provider]) { - $scope.accountList[i].status = 'linked'; + if (newValue && newValue != oldValue) { + angular.forEach(_accountList, function(account) { + var _linkedAccount = _.find(newValue, function(p) { + return p.provider === account.provider + }); + if (!_linkedAccount) { + account.status = 'unlinked'; + } else if(_linkedAccount.status && _linkedAccount.status.toLowerCase() === 'pending') { + account.status = 'pending'; } else { - $scope.accountList[i].status = 'pending'; + account.status = 'linked'; } - } + }); + $scope.accountList = _accountList; } }); @@ -89,11 +91,11 @@ .then(function(resp) { $log.debug("Social account unlinked: " + JSON.stringify(resp)); var toRemove = _.findIndex($scope.linkedAccounts, function(la) { - return la.providerType === provider.provider; + return la.provider === provider.provider; }); // remove from both links array and links data array $scope.linkedAccounts.splice(toRemove, 1); - delete $scope.linksData[provider.provider]; + // delete $scope.linksData[provider.provider]; toaster.pop('success', "Success", String.supplant( "Your {provider} account has been unlinked.", @@ -117,106 +119,6 @@ ] }; }) - .directive('externalLinksData', function() { - return { - restrict: 'E', - templateUrl: 'directives/external-account/external-link-data.directive.html', - scope: { - linkedAccountsData: '=', - externalLinks: '=' - }, - controller: ['$log', '$scope', 'ExternalAccountService', - function($log, $scope, ExternalAccountService) { - $log = $log.getInstance('ExternalLinksDataDirective'); - function reCalcData(links, data) { - $scope.linkedAccounts = []; - angular.forEach(links, function(link) { - var provider = link.providerType; - - if (!data[provider]) { - $scope.linkedAccounts.push({ - provider: provider, - data: { - handle: link.name, - status: 'PENDING' - } - }); - } else { - // add data - $scope.linkedAccounts.push({ - provider: provider, - data: data[provider] - }); - } - }); - } - - - $scope.$watch('linkedAccountsData', function(newValue, oldValue) { - reCalcData($scope.externalLinks, newValue); - }); - - $scope.$watchCollection('externalLinks', function(newValue, oldValue) { - reCalcData(newValue, $scope.linkedAccountsData); - }); - } - ] - } - }) - .directive('externalWebLink', function() { - return { - restrict: 'E', - templateUrl: 'directives/external-account/external-web-link.directive.html', - scope: { - linkedAccounts: '=', - userData: "=" - }, - controller: ['$log', '$scope', 'ExternalAccountService', - function($log, $scope, ExternalAccountService) { - $log = $log.getInstance('ExternalWebLinkDirective'); - $scope.addingWebLink = false; - $scope.errorMessage = null; - - $log.debug("userData: " + $scope.userData.handle); - - $scope.addWebLink = function() { - $log.debug("URL: " + $scope.url); - $scope.addingWebLink = true; - $scope.errorMessage = null; - ExternalAccountService.addWebLink($scope.userData.userId, $scope.userData.handle, $scope.url) - .then(function(resp) { - $scope.addingWebLink = false; - $log.debug("Web link added: " + JSON.stringify(resp)); - $scope.linkedAccounts.push(resp.profile); - toaster.pop('success', "Success", - String.supplant( - "Your link has been added. Data from your link will be visible on your profile shortly.", - {provider: extAccountProvider} - ) - ); - }) - .catch(function(resp) { - $scope.addingWebLink = false; - $log.debug(JSON.stringify(resp)); - if (resp.status === 'SOCIAL_PROFILE_ALREADY_EXISTS') { - $log.info("Social profile already linked to another account"); - toaster.pop('error', "Whoops!", - String.supplant( - "This {provider} account is linked to another account. \ - If you think this is an error please contact support@apprio.com.", - {provider: extAccountProvider } - ) - ); - } else { - $log.info("Server error:" + resp.content); - $scope.errorMessage = "Sorry, we are unable add web link. If problem persist please contact support@topcoder.com"; - } - }); - }; - } - ] - } - }) .filter('providerData', function() { return function(input, field) { return _.result(_.find(_supportedAccounts, function(s) { diff --git a/app/directives/external-account/external-account.directive.spec.js b/app/directives/external-account/external-account.directive.spec.js index 0698616a5..634d49257 100644 --- a/app/directives/external-account/external-account.directive.spec.js +++ b/app/directives/external-account/external-account.directive.spec.js @@ -14,37 +14,40 @@ describe('External Accounts Directive', function() { describe('Linked external accounts', function() { var linkedAccounts = [ { - providerType: 'linkedin', + provider: 'linkedin', // don't care about other details }, { - providerType: 'github' + provider: 'github' } ]; - var linksData = { - 'linkedin' : {provider: 'linkedin', name: 'name-linkedin'}, - 'github' : {provider: 'github', name: 'name-github'} - }; var externalAccounts; beforeEach(function() { scope.linkedAccounts = linkedAccounts; - scope.linksData = linksData; - element = angular.element(')'); + element = angular.element(')'); externalAccounts = $compile(element)(scope); - scope.$digest(); - // scope.$apply(); + // externalAccounts.scope().$digest(); + // externalAccounts.scope().$apply(); }); it('should have added account list to scope', function() { - expect(element.isolateScope().accountList).to.exist; - + inject(function($timeout) { + scope.$digest(); + $timeout(function() { + expect(element.isolateScope().accountList).to.exist; + }, 0) + }); }); it('should have "linked" property set for github & linkedin', function() { - var githubAccount = _.find(element.isolateScope().accountList, function(a) { return a.provider === 'github'}); - expect(githubAccount).to.have.property('status') - .that.equals('linked'); + inject(function($timeout) { + scope.$digest(); + $timeout(function() { + var githubAccount = _.find(element.isolateScope().accountList, function(a) { return a.provider === 'github'}); + expect(githubAccount).to.have.property('status').that.equals('linked'); + }, 0) + }); // var linkeindAccount = _.find(element.isolateScope().accountList, function(a) { return a.provider === 'linkedin'}); // expect(linkeindAccount).to.have.property('linked') @@ -52,89 +55,3 @@ describe('External Accounts Directive', function() { }); }); }); - -describe('External Links Data Directive', function() { - var scope; - var element; - - beforeEach(function() { - bard.appModule('topcoder'); - bard.inject(this, '$compile', '$rootScope'); - scope = $rootScope.$new(); - }); - - bard.verifyNoOutstandingHttpRequests(); - - describe('Linked external accounts', function() { - var externalLinks = [ - { - providerType: 'linkedin', - // don't care about other details - }, - { - providerType: 'github' - }, - { - providerType: 'behance' - }, - { - providerType: 'dribbble' - }, - { - providerType: 'bitbucket' - } - ]; - var linkedAccounts = { - github: { - handle: "github-handle", - followers: 1, - publicRepos: 1 - }, - stackoverflow: { - handle: 'so-handle', - reputation: 2, - answers: 2 - }, - behance: { - name: 'behance name', - projectViews: 3, - projectAppreciations: 3 - }, - dribbble: { - handle: 'dribble-handle', - followers: 4, - likes: 4 - }, - bitbucket: { - username: 'bitbucket-username', - followers: 5, - repositories: 5 - }, - twitter: { - handle: 'twitter-handle', - noOfTweets: 6, - followers: 6 - }, - linkedin: { - name: 'linkedin name', - title: 'linkedin title' - } - }; - var externalLinksData; - - beforeEach(function() { - scope.linkedAccounts = linkedAccounts; - scope.externalLinks = externalLinks; - element = angular.element(')'); - externalLinksData = $compile(element)(scope); - scope.$digest(); - }); - - it('should have added linkedAccounts to scope', function() { - expect(element.isolateScope().linkedAccounts).to.exist; - // linkedAccounts should have 5 entries because externalLinks contains only 5 records - expect(element.isolateScope().linkedAccounts).to.have.length(5); - }); - - }); -}); diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index 84c92f583..bb28878e6 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -1,5 +1,5 @@ .external-link-list - div.external-link-tile(ng-repeat="account in linkedAccounts") + div.external-link-tile(ng-repeat="account in linkedAccountsData") .top div.logo i.fa(ng-class="(account|providerData:'className') || 'fa-globe'") @@ -98,7 +98,7 @@ .title {{account.data.title}} - div(ng-switch-default) + div(ng-switch-when="weblink") .handle My Portfoilo - .title www.TBD.com + .title {{account.data.title}} diff --git a/app/directives/external-account/external-links-data.directive.js b/app/directives/external-account/external-links-data.directive.js new file mode 100644 index 000000000..fdb77f690 --- /dev/null +++ b/app/directives/external-account/external-links-data.directive.js @@ -0,0 +1,22 @@ +(function() { + 'use strict'; + + /** + * @desc links data card directive + * @example + */ + angular + .module('tcUIComponents') + .directive('externalLinksData', externalLinksData); + + function externalLinksData() { + var directive = { + restrict: 'E', + templateUrl: 'directives/external-account/external-link-data.directive.html', + scope: { + linkedAccountsData: '=' + } + }; + 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 new file mode 100644 index 000000000..81f085ef9 --- /dev/null +++ b/app/directives/external-account/external-links-data.directive.spec.js @@ -0,0 +1,85 @@ +/* jshint -W117, -W030 */ +describe('External Links Data Directive', function() { + var scope; + var element; + + beforeEach(function() { + bard.appModule('topcoder'); + bard.inject(this, '$compile', '$rootScope'); + scope = $rootScope.$new(); + }); + + 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 externalLinksData; + + beforeEach(function() { + scope.linkedAccounts = linkedAccounts; + element = angular.element(')'); + externalLinksData = $compile(element)(scope); + scope.$digest(); + }); + + it('should have added linkedAccounts to scope', function() { + expect(element.isolateScope().linkedAccountsData).to.exist; + expect(element.isolateScope().linkedAccountsData).to.have.length(7); + }); + + }); +}); diff --git a/app/directives/external-account/external-web-link.directive.jade b/app/directives/external-account/external-web-link.directive.jade index ec0402474..8d8478ae0 100644 --- a/app/directives/external-account/external-web-link.directive.jade +++ b/app/directives/external-account/external-web-link.directive.jade @@ -1,14 +1,17 @@ .web-link - form(name="addWebLinkFrm", ng-submit="addWebLinkFrm.$valid && addWebLink()", autocomplete="off") + form(name="addWebLinkFrm", ng-submit="addWebLinkFrm.$valid && vm.addWebLink()", autocomplete="off") .validation-bar.url(ng-class="{ 'error-bar': (addWebLinkFrm.url.$dirty && addWebLinkFrm.url.$invalid) }") - input.form-field.url(name="url", type="url", ng-model="url", placeholder="http://www.youlink.com", required) + input.form-field.url(name="url", type="url", ng-model="vm.url", placeholder="http://www.yourlink.com", required) .form-input-error(ng-show="addWebLinkFrm.url.$dirty && addWebLinkFrm.url.$invalid") p(ng-show="addWebLinkFrm.url.$error.required") This is a required field. - p(ng-show="addWebLinkFrm.url.$error.url") Please enter a vadlid URL + p(ng-show="addWebLinkFrm.url.$error.url") Please enter a valid URL - button(type="submit", tc-busy-button, tc-busy-when="addingWebLink", tc-busy-message="Adding", ng-disabled="addWebLinkFrm.$invalid || addWebLinkFrm.$pristine", ng-class="{'enabled-button': addWebLinkFrm.$valid, 'disabled': addWebLinkFrm.$pristine || addWebLinkFrm.$invalid, 'busy' : addingWebLink}") Add + button.tc-btn.tc-btn-secondary.tc-btn-s(type="submit", + tc-busy-button, tc-busy-when="vm.addingWebLink", tc-busy-message="Adding", + ng-disabled="addWebLinkFrm.$invalid || addWebLinkFrm.$pristine", + ng-class="{'enabled-button': addWebLinkFrm.$valid, 'disabled': addWebLinkFrm.$pristine || addWebLinkFrm.$invalid, 'busy' : vm.addingWebLink}") Add - .form-errors(ng-show="errorMessage") - p.form-error {{errorMessage}} \ No newline at end of file + .form-errors(ng-show="vm.errorMessage") + p.form-error {{vm.errorMessage}} diff --git a/app/directives/external-account/external-web-links.directive.js b/app/directives/external-account/external-web-links.directive.js new file mode 100644 index 000000000..aae814aea --- /dev/null +++ b/app/directives/external-account/external-web-links.directive.js @@ -0,0 +1,69 @@ +(function() { + 'use strict'; + + /** + * @desc links data card directive + * @example + */ + angular + .module('tcUIComponents') + .directive('externalWebLink', externalWebLink); + + function externalWebLink() { + var directive = { + estrict: 'E', + templateUrl: 'directives/external-account/external-web-link.directive.html', + scope: { + linkedAccounts: '=', + userHandle: '@' + }, + controller: ['$log', '$scope', 'ExternalWebLinksService', ExternalWebLinkCtrl], + controllerAs: 'vm', + bindToController: true + }; + return directive; + + function ExternalWebLinkCtrl($log, $scope, ExternalWebLinksService) { + $log = $log.getInstance('ExternalWebLinkCtrl'); + var vm = this; + vm.addingWebLink = false; + vm.errorMessage = null; + + vm.addWebLink = function() { + $log.debug("URL: " + vm.url); + vm.addingWebLink = true; + vm.errorMessage = null; + ExternalWebLinksService.addLink(vm.userHandle, vm.url) + .then(function(resp) { + vm.addingWebLink = false; + $log.debug("Web link added: " + JSON.stringify(resp)); + vm.linkedAccounts.push(resp.profile); + toaster.pop('success', "Success", + String.supplant( + "Your link has been added. Data from your link will be visible on your profile shortly.", + {provider: extAccountProvider} + ) + ); + }) + .catch(function(resp) { + vm.addingWebLink = false; + $log.debug(JSON.stringify(resp)); + if (resp.status === 'SOCIAL_PROFILE_ALREADY_EXISTS') { + $log.info("Social profile already linked to another account"); + toaster.pop('error', "Whoops!", + String.supplant( + "This {provider} account is linked to another account. \ + If you think this is an error please contact support@apprio.com.", { + provider: extAccountProvider + } + ) + ); + } else { + $log.info("Server error:" + resp.content); + vm.errorMessage = "Sorry, we are unable add web link. If problem persist please contact support@topcoder.com"; + } + }); + }; + } + } +})(); diff --git a/app/filters/external-link-color.filter.js b/app/filters/external-link-color.filter.js index 9a81f4033..5dc7c9578 100644 --- a/app/filters/external-link-color.filter.js +++ b/app/filters/external-link-color.filter.js @@ -10,7 +10,7 @@ var providerColors = { 'el-weblinks': '#82A0AA', 'el-bitbucket': '#205081', - 'el-dribble': '#EA4C89', + 'el-dribbble': '#EA4C89', 'el-linkedin': '#127CB5', 'el-twitter': '#62AADC', 'el-stackoverflow': '#E5712A', diff --git a/app/index.jade b/app/index.jade index c724d5849..33163c022 100644 --- a/app/index.jade +++ b/app/index.jade @@ -182,6 +182,8 @@ html script(src="directives/challenge-user-place/challenge-user-place.directive.js") script(src="directives/distribution-graph/distribution-graph.directive.js") script(src="directives/external-account/external-account.directive.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") script(src="directives/header/header-menu-item.directive.js") script(src="directives/history-graph/history-graph.directive.js") @@ -258,6 +260,7 @@ html script(src="services/blog.service.js") script(src="services/challenge.service.js") script(src="services/externalAccounts.service.js") + script(src="services/externalLinks.service.js") script(src="services/helpers.service.js") script(src="services/image.service.js") script(src="services/introduction.service.js") diff --git a/app/my-dashboard/subtrack-stats/subtrack-stats.jade b/app/my-dashboard/subtrack-stats/subtrack-stats.jade index c9c135e8a..7a8c1c582 100644 --- a/app/my-dashboard/subtrack-stats/subtrack-stats.jade +++ b/app/my-dashboard/subtrack-stats/subtrack-stats.jade @@ -15,7 +15,7 @@ p {{subtrack.statType}} responsive-carousel(data="vm.subtrackRanks", handle="{{vm.handle}}") - .track(ui-sref="profile.subtrack({userHandle: handle, track: item.track, subTrack: item.subTrack})") + a.track(ui-sref="profile.subtrack({userHandle: handle, track: item.track, subTrack: item.subTrack})") .flex-wrapper p.subtrack(title="{{item.subTrack | underscoreStrip}}") {{item.subTrack | underscoreStrip}} diff --git a/app/profile/about/about.controller.js b/app/profile/about/about.controller.js index 04d077095..8a77b01a5 100644 --- a/app/profile/about/about.controller.js +++ b/app/profile/about/about.controller.js @@ -3,9 +3,9 @@ angular.module('tc.profile').controller('ProfileAboutController', ProfileAboutController); - ProfileAboutController.$inject = ['$log', '$scope', 'ProfileService', 'ExternalAccountService', 'UserService', 'CONSTANTS']; + ProfileAboutController.$inject = ['$log', '$scope', '$q', 'ProfileService', 'ExternalAccountService', 'ExternalWebLinksService', 'UserService', 'CONSTANTS']; - function ProfileAboutController($log, $scope, ProfileService, ExternalAccountService, UserService, CONSTANTS) { + function ProfileAboutController($log, $scope, $q, ProfileService, ExternalAccountService, ExternalWebLinksService, UserService, CONSTANTS) { var vm = this; $log = $log.getInstance("ProfileAboutController"); var profileVm = $scope.$parent.profileVm; @@ -22,38 +22,18 @@ function activate() { - ExternalAccountService.getLinkedExternalLinksData(profileVm.userHandle).then(function(data) { - vm.linkedExternalAccountsData = data.plain(); - - // show section if user is viewing his/her own profile OR if we have data - //vm.hasLinks = profileVm.linkedExternalAccounts.length; - vm.hasLinks = _.any(_.valuesIn(_.omit(vm.linkedExternalAccountsData, ['userId', 'updatedAt','createdAt','createdBy','updatedBy','handle']))); - vm.displaySection.externalLinks = profileVm.showEditProfileLink || vm.hasLinks; - - // if user is authenticated, call for profiles end point - if (profileVm.isUser) { - var userId = UserService.getUserIdentity().userId; - ExternalAccountService.getLinkedExternalAccounts(userId).then(function(data) { - vm.linkedExternalAccounts = data; - profileVm.status.externalLinks = CONSTANTS.STATE_READY; - }).catch(function(err) { - profileVm.status.externalLinks = CONSTANTS.STATE_ERROR; - }); - } else { - vm.linkedExternalAccounts = []; - // remove all keys except the provider keys - var accounts = _.omit(vm.linkedExternalAccountsData, ['userId', 'updatedAt','createdAt','createdBy','updatedBy','handle']); - // populate the externalLinks for external-account-data directive with info from ext accounts data - for(var provider in accounts) { - if (accounts[provider]) { - vm.linkedExternalAccounts.push({ - providerType: provider - }); - } - } - profileVm.status.externalLinks = CONSTANTS.STATE_READY; - } - }).catch(function(err) { + var _userId = profileVm.isUser ? UserService.getUserIdentity().userId : false; + // retrieve web links & external accounts + var _linksPromises = [ + ExternalAccountService.getAllExternalLinks(profileVm.userHandle, _userId, !!_userId), + ExternalWebLinksService.getLinks(profileVm.userHandle, !!_userId) + ]; + $q.all(_linksPromises).then(function(data) { + vm.linkedExternalAccounts = data[0].concat(data[1]); + vm.displaySection.externalLinks = profileVm.showEditProfileLink || !!vm.linkedExternalAccounts.length; + + profileVm.status.externalLinks = CONSTANTS.STATE_READY; + }).catch(function(resp) { profileVm.status.externalLinks = CONSTANTS.STATE_ERROR; }); diff --git a/app/profile/about/about.jade b/app/profile/about/about.jade index ce27b0114..490215927 100644 --- a/app/profile/about/about.jade +++ b/app/profile/about/about.jade @@ -98,9 +98,10 @@ tc-section(ng-show="vm.displaySection.externalLinks", state="profileVm.status.externalLinks") .external-links h3.activity on the web - external-links-data(ng-show="vm.hasLinks", external-links="vm.linkedExternalAccounts", linked-accounts-data="vm.linkedExternalAccountsData") + + external-links-data(ng-show="vm.linkedExternalAccounts.length", linked-accounts-data="vm.linkedExternalAccounts") - .empty-state(ng-hide="vm.hasLinks") + .empty-state(ng-hide="vm.linkedExternalAccounts.length") .action-text Showcase your work from around the web external-accounts.external-account-container(linked-accounts="[]", links-data="{}", read-only="true") diff --git a/app/services/externalAccounts.service.js b/app/services/externalAccounts.service.js index a75fd6e73..0a03dc09b 100644 --- a/app/services/externalAccounts.service.js +++ b/app/services/externalAccounts.service.js @@ -9,15 +9,15 @@ var auth0 = auth; $log = $log.getInstance('ExternalAccountService'); - var api = ApiService.restangularV3; + var memberApi = ApiService.getApiServiceProvider('MEMBER'); var userApi = ApiService.getApiServiceProvider('USER'); var service = { - getLinkedExternalAccounts: getLinkedExternalAccounts, - getLinkedExternalLinksData: getLinkedExternalLinksData, + getAllExternalLinks: getAllExternalLinks, + getLinkedAccounts: getLinkedAccounts, + getAccountsData: getAccountsData, linkExternalAccount: linkExternalAccount, - unlinkExternalAccount: unlinkExternalAccount, - addWebLink : addWebLink + unlinkExternalAccount: unlinkExternalAccount }; return service; @@ -29,22 +29,27 @@ * @param userId * @return list of linked Accounts */ - function getLinkedExternalAccounts(userId) { + function getLinkedAccounts(userId) { return userApi.one('users', userId).get({fields:"profiles"}) .then(function(result) { - return result.profiles || []; + angular.forEach(result.profiles, function(p) { + p.provider = p.providerType; + }); + return result.profiles; }); } - function getLinkedExternalLinksData(userHandle) { - return api.one('members', userHandle).withHttpConfig({skipAuthorization: true}).customGET('externalAccounts') - .then(function(data) { - // TODO workaround for dribbble spelling mistake, remove once API is fixed - if (data.dribble) { - data.dribbble = data.dribble; - } - return data; - }) + function getAccountsData(userHandle) { + return memberApi.one('members', userHandle) + .withHttpConfig({skipAuthorization: true}) + .customGET('externalAccounts') + .then(function(data) { + // TODO workaround for dribbble spelling mistake, remove once API is fixed + if (data.dribble) { + data.dribbble = data.dribble; + } + return data; + }); } function unlinkExternalAccount(account) { @@ -74,19 +79,53 @@ }); } - function addWebLink(userId, userHandle, url) { - var provider = "url::" + url; - var postData = { - userId: userId, - providerType: provider, - context: { - handle: userHandle + + function _convertAccountsIntoCards(links, data, includePending) { + var _cards = []; + if (!links.length) { + var providers = _.omit(data, ['userId', 'updatedAt', 'createdAt', 'createdBy', 'updatedBy', 'handle']); + // populate the externalLinks for external-account-data directive with info from ext accounts data + + angular.forEach(_.keys(providers), function(p) { + if (providers[p]) + links.push({provider: p}); + }); + } + // handling external accounts first + angular.forEach(links, function(link) { + var provider = link.provider; + if (data[provider]) { + // add data + _cards.push({provider: provider, data: data[provider]}); + } else if (includePending) { + // add pending card + _cards.push({provider: provider, data: {handle: link.name, status: 'PENDING'}}); } - }; - // delegates to user service - return UserService.addSocialProfile(userId, postData); + }); + $log.debug("Processed Accounts Cards: " + JSON.stringify(_cards)); + return _cards; } + + function getAllExternalLinks(userHandle, userId, includePending) { + return $q(function(resolve, reject) { + var _promises = [getAccountsData(userHandle)]; + if (includePending) + _promises.push(getLinkedAccounts(userId)); + + $q.all(_promises).then(function(data) { + var links = includePending ? data[1]: []; + var _cards = _convertAccountsIntoCards(links, data[0].plain(), includePending); + // TODO add weblinks + resolve(_cards); + }).catch(function(resp) { + $log.error(resp); + reject(resp); + }) + }) + } + + function linkExternalAccount(provider, callbackUrl) { return $q(function(resolve, reject) { // supported backends diff --git a/app/services/externalAccounts.service.spec.js b/app/services/externalAccounts.service.spec.js new file mode 100644 index 000000000..408f89f4c --- /dev/null +++ b/app/services/externalAccounts.service.spec.js @@ -0,0 +1,76 @@ +/* jshint -W117, -W030 */ +describe('ExternalAccount Service', function() { + var service; + var mockAccountsData = mockData.getMockLinkedExternalAccountsData(); + var mockUserLinksData = mockData.getMockLinkedExternalAccounts(); + var apiUrl; + + + beforeEach(function() { + bard.appModule('topcoder'); + bard.inject(this, 'ExternalAccountService', '$httpBackend', '$q', 'CONSTANTS', 'JwtInterceptorService'); + bard.mockService(JwtInterceptorService, { + getToken: $q.when('token'), + _default: $q.when([]) + }); + + apiUrl = CONSTANTS.API_URL; + service = ExternalAccountService; + + $httpBackend + .when('GET', apiUrl + '/members/test1/externalAccounts/') + .respond(200, {result: {content: mockAccountsData}}); + $httpBackend + .when('GET', apiUrl + '/users/111/?fields=profiles') + .respond(200, {result: {content: mockUserLinksData}}); + + }); + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('service should be defined', function() { + expect(service).to.be.defined; + }); + + it('should return linked external accounts', function() { + service.getLinkedAccounts(111).then(function(data) { + expect(data).to.have.length(5); + }); + $httpBackend.flush(); + }); + + it('should return linked external accounts data', function() { + service.getAccountsData('test1').then(function(data) { + data = data.plain(); + expect(data).to.be.defined; + expect(_.keys(data)).to.include.members(['dribbble', 'github', 'behance', 'bitbucket', 'linkedin', 'stackoverflow', 'twitter']); + }); + $httpBackend.flush(); + }); + + it('should return all non-pending external links', function() { + // spy + service.getAllExternalLinks('test1', 111, false).then(function(data) { + expect(data).to.be.defined; + expect(_.pluck(data, 'provider')).to.include.members(['dribbble', 'github','bitbucket', 'stackoverflow']); + expect(_.all(_.pluck(data, 'data'))).to.be.truthy; + }); + $httpBackend.flush(); + }); + + it('should return all external links including pending', function() { + // spy + service.getAllExternalLinks('test1', 111, true).then(function(data) { + expect(data).to.be.defined; + expect(_.pluck(data, 'provider')).to.include.members(['dribbble', 'github', 'behance', 'bitbucket','stackoverflow']); + expect(data).to.have.length(5); + var nullAccounts = _.remove(data, function(n) {return n.data.status === 'PENDING'}); + expect(nullAccounts).to.have.length(1); + }); + $httpBackend.flush(); + }); + +}); diff --git a/app/services/externalLinks.service.js b/app/services/externalLinks.service.js new file mode 100644 index 000000000..c86d13d5d --- /dev/null +++ b/app/services/externalLinks.service.js @@ -0,0 +1,50 @@ +(function() { + 'use strict'; + + angular.module('tc.services').factory('ExternalWebLinksService', ExternalWebLinksService); + + ExternalWebLinksService.$inject = ['$log', 'CONSTANTS', 'ApiService']; + + function ExternalWebLinksService($log, CONSTANTS, ApiService) { + $log = $log.getInstance("ExternalWebLinksService"); + + var memberApi = ApiService.getApiServiceProvider('MEMBER'); + + var service = { + getLinks: getLinks, + addLink: addLink, + removeLink: removeLink + }; + return service; + + ///////////////////////// + + function getLinks(userHandle, includePending) { + return memberApi.one('members', userHandle) + .withHttpConfig({skipAuthorization: true}) + .customGET('externalLinks') + .then(function(links) { + links = links.plain(); + if (!includePending) { + _.remove(links, function(l) { + return l.status && l.status.toLowerCase() === 'pending'; + }); + } + links = _(links).forEach(function(l) { + l.provider = 'weblink'; + }).value(); + // add provider type as link + return links; + }); + } + + function addLink(userHandle, url) { + return memberApi.one('members', userHandle).customPOST({'url': url}, 'externalLinks'); + } + + function removeLink(userHandle, key) { + return memberApi.one('members', userHandle).one('externalLinks', key).delete(); + } + + } +})(); diff --git a/app/services/externalLinks.service.spec.js b/app/services/externalLinks.service.spec.js new file mode 100644 index 000000000..e6fb1723e --- /dev/null +++ b/app/services/externalLinks.service.spec.js @@ -0,0 +1,44 @@ +/* jshint -W117, -W030 */ +describe('ExternalWebLinks service', function() { + var service; + var mockExternalLinks = mockData.getMockExternalWebLinksData(); + var apiUrl; + + + beforeEach(function() { + bard.appModule('topcoder'); + bard.inject(this, 'ExternalWebLinksService', '$httpBackend', 'CONSTANTS'); + + apiUrl = CONSTANTS.API_URL; + service = ExternalWebLinksService; + + // mock profile api + $httpBackend + .when('GET', apiUrl + '/members/test1/externalLinks/') + .respond(200, {result: {content: mockExternalLinks}}); + }); + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should be defined', function() { + expect(service).to.be.defined; + }); + + it('should return linked external web links including pending', function() { + service.getLinks('test1', true).then(function(data) { + expect(data).to.have.length(3); + }); + $httpBackend.flush(); + }); + + it('should return linked external non-pending web links ', function() { + service.getLinks('test1', false).then(function(data) { + expect(data).to.have.length(2); + }); + $httpBackend.flush(); + }); + +}); diff --git a/app/services/user.service.js b/app/services/user.service.js index c7ec8518c..b1006636f 100644 --- a/app/services/user.service.js +++ b/app/services/user.service.js @@ -25,7 +25,7 @@ updatePassword: updatePassword, getUserProfile: getUserProfile, getV2UserProfile: getV2UserProfile, - addSocialProfile: addSocialProfile + addSocialProfile: addSocialProfile, removeSocialProfile: removeSocialProfile, getPreference: getPreference, setPreference: setPreference diff --git a/app/settings/edit-profile/edit-profile.controller.js b/app/settings/edit-profile/edit-profile.controller.js index 947731dd2..df2dbd230 100644 --- a/app/settings/edit-profile/edit-profile.controller.js +++ b/app/settings/edit-profile/edit-profile.controller.js @@ -4,9 +4,9 @@ angular.module('tc.settings').controller('EditProfileController', EditProfileController); - EditProfileController.$inject = ['userData', 'userHandle', 'ProfileService', 'ExternalAccountService', '$log', 'ISO3166', 'ImageService', 'CONSTANTS', 'TagsService', 'toaster']; + EditProfileController.$inject = ['userData', 'userHandle', 'ProfileService', 'ExternalAccountService', 'ExternalWebLinksService', '$log', 'ISO3166', 'ImageService', 'CONSTANTS', 'TagsService', 'toaster', '$q']; - function EditProfileController(userData, userHandle, ProfileService, ExternalAccountService, $log, ISO3166, ImageService, CONSTANTS, TagsService, toaster) { + function EditProfileController(userData, userHandle, ProfileService, ExternalAccountService, ExternalWebLinksService, $log, ISO3166, ImageService, CONSTANTS, TagsService, toaster, $q) { $log = $log.getInstance("EditProfileCtrl"); var vm = this; vm.toggleTrack = toggleTrack; @@ -34,17 +34,30 @@ processData(vm.userData); - ExternalAccountService.getLinkedExternalAccounts(vm.userData.userId).then(function(data) { - vm.linkedExternalAccounts = data; - }); - - ExternalAccountService.getLinkedExternalLinksData(userHandle).then(function(data) { - vm.linkedExternalAccountsData = data.plain(); - vm.hasLinks = _.any(_.valuesIn(_.omit(vm.linkedExternalAccountsData, ['userId', 'updatedAt','createdAt','createdBy','updatedBy','handle']))); - }) - .catch(function(err) { - $log.error(JSON.stringify(err)); + // ExternalAccountService.getLinkedExternalAccounts(vm.userData.userId).then(function(data) { + // vm.linkedExternalAccounts = data; + // }); + + // ExternalAccountService.getLinkedExternalLinksData(userHandle).then(function(data) { + // vm.linkedExternalAccountsData = data.plain(); + // vm.hasLinks = _.any(_.valuesIn(_.omit(vm.linkedExternalAccountsData, ['userId', 'updatedAt','createdAt','createdBy','updatedBy','handle']))); + // }) + // .catch(function(err) { + // $log.error(JSON.stringify(err)); + // }); + var userId = vm.userData.userId; + var userHandle = vm.userData.handle; + var _linksPromises = [ + ExternalAccountService.getAllExternalLinks(userHandle, userId, true), + ExternalWebLinksService.getLinks(userHandle, true) + ]; + $q.all(_linksPromises).then(function(data) { + vm.linkedExternalAccountsData = data[0].concat(data[1]); }); + ExternalAccountService.getLinkedAccounts(userId) + .then(function(data) { + vm.linkedExternalAccounts = data; + }) TagsService.getApprovedSkillTags() .then(function(tags) { diff --git a/app/settings/edit-profile/edit-profile.jade b/app/settings/edit-profile/edit-profile.jade index 7959ca40f..457d3e03f 100644 --- a/app/settings/edit-profile/edit-profile.jade +++ b/app/settings/edit-profile/edit-profile.jade @@ -97,14 +97,14 @@ .field-label Add a web link .web-links - external-web-link(linked-accounts="vm.linkedExternalAccounts", user-data="vm.userData", read-only="false") + external-web-link(linked-accounts="vm.linkedExternalAccounts", user-handle="{{vm.userData.handle}}") .field-label Link Your Accounts .external-links - external-accounts(linked-accounts="vm.linkedExternalAccounts", links-data="vm.linkedExternalAccountsData", read-only="false") + external-accounts(linked-accounts="vm.linkedExternalAccountsData", read-only="false") .field-label Linked Accounts .existing-links - external-links-data(external-links="vm.linkedExternalAccounts", linked-accounts-data="vm.linkedExternalAccountsData") + external-links-data(linked-accounts-data="vm.linkedExternalAccountsData") diff --git a/app/specs.html b/app/specs.html index 567c25f72..a1b55ba8b 100644 --- a/app/specs.html +++ b/app/specs.html @@ -74,6 +74,8 @@

Spec Runner

+ + @@ -111,11 +113,13 @@

Spec Runner

+ + @@ -155,6 +159,7 @@

Spec Runner

+ @@ -173,6 +178,8 @@

Spec Runner

+ + @@ -181,16 +188,20 @@

Spec Runner

+ + + + @@ -223,6 +234,8 @@

Spec Runner

+ + @@ -231,21 +244,23 @@

Spec Runner

- + + - + + - + @@ -253,8 +268,8 @@

Spec Runner

- + diff --git a/assets/css/directives/external-web-link.scss b/assets/css/directives/external-web-link.scss index ca355e62e..66544f358 100644 --- a/assets/css/directives/external-web-link.scss +++ b/assets/css/directives/external-web-link.scss @@ -46,23 +46,23 @@ external-web-link { margin: 0px; } - button { - @include button-2-m; - font-size: 14px; - line-height: 16px; - height: 40px; - width: 60px; - margin: 0 0 0 10px; - text-transform: uppercase; + // button { + // @include button-2-m; + // font-size: 14px; + // line-height: 16px; + // height: 40px; + // width: 60px; + // margin: 0 0 0 10px; + // text-transform: uppercase; - &.enabled-button { - @include ui-enabled-button; - } + // &.enabled-button { + // @include ui-enabled-button; + // } - &.busy { - width: 94px; - } - } + // &.busy { + // width: 94px; + // } + // } } } -} \ No newline at end of file +} diff --git a/tests/test-helpers/mock-data.js b/tests/test-helpers/mock-data.js index c7f435066..858b3922d 100644 --- a/tests/test-helpers/mock-data.js +++ b/tests/test-helpers/mock-data.js @@ -22,7 +22,8 @@ var mockData = (function() { getMockBadge: getMockBadge, getMockUserFinancials: getMockUserFinancials, getMockLinkedExternalAccounts: getMockLinkedExternalAccounts, - getMockLinkedExternalAccountsData: getMockLinkedExternalAccountsData + getMockLinkedExternalAccountsData: getMockLinkedExternalAccountsData, + getMockExternalWebLinksData: getMockExternalWebLinksData, }; function getMockStates() { @@ -1625,99 +1626,85 @@ var mockData = (function() { "DESIGN": { "challenges": 664, "wins": 271, - "subTracks": [ - { - "id": 34, - "name": "STUDIO_OTHER", - "challenges": 21, - "wins": 4, - "mostRecentEventDate": "2011-04-20T09:00:00.000Z" - }, - { - "id": 30, - "name": "WIDGET_OR_MOBILE_SCREEN_DESIGN", - "challenges": 82, - "wins": 30, - "mostRecentEventDate": "2015-02-01T22:00:53.000Z" - }, - { - "id": 22, - "name": "IDEA_GENERATION", - "challenges": 3, - "wins": 0, - "mostRecentEventDate": "2013-05-27T10:00:07.000Z" - }, - { - "id": 17, - "name": "WEB_DESIGNS", - "challenges": 418, - "wins": 190, - "mostRecentEventDate": "2015-01-26T19:00:03.000Z" - }, - { - "id": 29, - "name": "COPILOT_POSTING", - "challenges": 1, - "wins": 0, - "mostRecentEventDate": null - }, - { - "id": 18, - "name": "WIREFRAMES", - "challenges": 2, - "wins": 1, - "mostRecentEventDate": "2010-11-17T09:00:00.000Z" - }, - { - "id": 14, - "name": "ASSEMBLY_COMPETITION", - "challenges": 1, - "wins": 0, - "mostRecentEventDate": null - }, - { - "id": 32, - "name": "APPLICATION_FRONT_END_DESIGN", - "challenges": 54, - "wins": 23, - "mostRecentEventDate": "2014-08-07T01:03:11.000Z" - }, - { - "id": 21, - "name": "PRINT_OR_PRESENTATION", - "challenges": 24, - "wins": 8, - "mostRecentEventDate": "2014-10-08T17:48:09.000Z" - }, - { - "id": 16, - "name": "BANNERS_OR_ICONS", - "challenges": 24, - "wins": 10, - "mostRecentEventDate": "2014-01-11T17:30:27.000Z" - }, - { - "id": 20, - "name": "LOGO_DESIGN", - "challenges": 31, - "wins": 4, - "mostRecentEventDate": "2014-02-21T19:00:09.000Z" - }, - { - "id": 31, - "name": "FRONT_END_FLASH", - "challenges": 2, - "wins": 1, - "mostRecentEventDate": "2009-06-19T23:00:00.000Z" - }, - { - "id": 13, - "name": "TEST_SUITES", - "challenges": 1, - "wins": 0, - "mostRecentEventDate": null - } - ], + "subTracks": [{ + "id": 34, + "name": "STUDIO_OTHER", + "challenges": 21, + "wins": 4, + "mostRecentEventDate": "2011-04-20T09:00:00.000Z" + }, { + "id": 30, + "name": "WIDGET_OR_MOBILE_SCREEN_DESIGN", + "challenges": 82, + "wins": 30, + "mostRecentEventDate": "2015-02-01T22:00:53.000Z" + }, { + "id": 22, + "name": "IDEA_GENERATION", + "challenges": 3, + "wins": 0, + "mostRecentEventDate": "2013-05-27T10:00:07.000Z" + }, { + "id": 17, + "name": "WEB_DESIGNS", + "challenges": 418, + "wins": 190, + "mostRecentEventDate": "2015-01-26T19:00:03.000Z" + }, { + "id": 29, + "name": "COPILOT_POSTING", + "challenges": 1, + "wins": 0, + "mostRecentEventDate": null + }, { + "id": 18, + "name": "WIREFRAMES", + "challenges": 2, + "wins": 1, + "mostRecentEventDate": "2010-11-17T09:00:00.000Z" + }, { + "id": 14, + "name": "ASSEMBLY_COMPETITION", + "challenges": 1, + "wins": 0, + "mostRecentEventDate": null + }, { + "id": 32, + "name": "APPLICATION_FRONT_END_DESIGN", + "challenges": 54, + "wins": 23, + "mostRecentEventDate": "2014-08-07T01:03:11.000Z" + }, { + "id": 21, + "name": "PRINT_OR_PRESENTATION", + "challenges": 24, + "wins": 8, + "mostRecentEventDate": "2014-10-08T17:48:09.000Z" + }, { + "id": 16, + "name": "BANNERS_OR_ICONS", + "challenges": 24, + "wins": 10, + "mostRecentEventDate": "2014-01-11T17:30:27.000Z" + }, { + "id": 20, + "name": "LOGO_DESIGN", + "challenges": 31, + "wins": 4, + "mostRecentEventDate": "2014-02-21T19:00:09.000Z" + }, { + "id": 31, + "name": "FRONT_END_FLASH", + "challenges": 2, + "wins": 1, + "mostRecentEventDate": "2009-06-19T23:00:00.000Z" + }, { + "id": 13, + "name": "TEST_SUITES", + "challenges": 1, + "wins": 0, + "mostRecentEventDate": null + }], "mostRecentEventDate": "2015-02-01T22:00:53.000Z" }, "DATA_SCIENCE": { @@ -1975,38 +1962,94 @@ var mockData = (function() { function getMockLinkedExternalAccountsData() { return { - github: null, - stackoverflow: null, - dribble: null, - behance: null, - bitbucket: null, - linkedin: null, - twitter: null, - userId: 123, + "updatedAt": null, + "createdAt": null, + "createdBy": null, + "updatedBy": null, + "userId": 22688955, + "handle": "test", + "behance": null, + "bitbucket": { + "handle": "test1", + "followers": 0, + "languages": "html/css", + "repos": 1 + }, + "dribbble": { + "handle": "test2", + "socialId": "944202", + "name": "Vikas Agarwal", + "summary": "Principal Engineer @Appirio", + "followers": 0, + "likes": 0, + "tags": null + }, + "github": { + "handle": "test3", + "socialId": "2417632", + "publicRepos": 11, + "followers": 2, + "languages": "Java,JavaScript,HTML,CSS,Ruby" + }, + "linkedin": null, + "stackoverflow": { + "name": "test", + "socialId": "365172", + "answers": 42, + "questions": 9, + "reputation": 928, + "topTags": "java,jsp,jstl,hashmap,quartz-scheduler,eclipse,ant,tomcat,warnings,hadoop,mysql,amazon-ec2,amazon-ebs,java-ee,amazon-web-services,amazon-rds,hibernate,scala,maven,apache-spark,apache-spark-sql,hbase,scheduling,javascript,gmail,junit,byte,persistence,hql,gdata" + }, + "twitter": null, plain: function() {} }; } function getMockLinkedExternalAccounts() { - return [ - { - providerType: 'linkedin', - // don't care about other details - }, - { + return { + profiles: [{ providerType: 'github' - }, - { + }, { + providerType: 'stackoverflow' + }, { providerType: 'behance' - }, - { + }, { providerType: 'dribbble' - }, - { + }, { providerType: 'bitbucket' - } - ]; + }] + } } + function getMockExternalWebLinksData() { + return [ + { + "userId": 111, + "key": "c69a1246c135b16069395010e91f5c64", + "handle": "test1", + "description": "description 1.", + "entities": "Activiti,Data Science,Reference Implementation for Angular Reference", + "keywords": "topcoder-app,merged,oct,dashboard,15appirio-tech,20appirio-tech,polish,21appirio-tech,sup-1889,19appirio-tech", + "title": "Test's profile", + "images": "https://avatars3.githubusercontent.com/u/2417632?v=3&s=400,https://avatars1.githubusercontent.com/u/2417632?v=3&s=460,https://assets-cdn.github.com/images/spinners/octocat-spinner-128.gif", + "source": "embed.ly" + }, { + "userId": 111, + "key": "c69a1246c135b16069395010e91f5c65", + "handle": "test1", + "description": "description 1.", + "entities": "Activiti,Data Science,Reference Implementation for Angular Reference", + "keywords": "topcoder-app,merged,oct,dashboard,15appirio-tech,20appirio-tech,polish,21appirio-tech,sup-1889,19appirio-tech", + "title": "Test's profile", + "images": "https://avatars3.githubusercontent.com/u/2417632?v=3&s=400,https://avatars1.githubusercontent.com/u/2417632?v=3&s=460,https://assets-cdn.github.com/images/spinners/octocat-spinner-128.gif", + "source": "embed.ly" + }, { + "userId": 111, + "key": "c69a1246c135b16069395010e91f5c66", + "handle": "test1", + "status": 'PENDING' + } + ] + } })(); From 5a62e3529612a8bfa7e7411f80999c2b8019ce68 Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Tue, 27 Oct 2015 14:03:36 -0700 Subject: [PATCH 03/12] more fixes --- .../challenge-tile.directive.jade | 8 ++- .../external-web-links.directive.js | 47 +++++-------- .../external-web-links.directive.spec.js | 67 +++++++++++++++++++ app/services/externalAccounts.service.js | 13 ++-- app/services/externalLinks.service.js | 15 ++++- app/specs.html | 9 +-- assets/css/directives/external-web-link.scss | 3 +- tests/test-helpers/mock-data.js | 8 ++- 8 files changed, 121 insertions(+), 49 deletions(-) create mode 100644 app/directives/external-account/external-web-links.directive.spec.js diff --git a/app/directives/challenge-tile/challenge-tile.directive.jade b/app/directives/challenge-tile/challenge-tile.directive.jade index 1b585353e..93721f3c8 100644 --- a/app/directives/challenge-tile/challenge-tile.directive.jade +++ b/app/directives/challenge-tile/challenge-tile.directive.jade @@ -31,7 +31,8 @@ // Only show if not data science track p.roles span(ng-hide="challenge.track === 'DATA_SCIENCE'") - #[span Role:  ] #[span {{challenge.userDetails.roles | listRoles}}] + span Role:   + span {{challenge.userDetails.roles | listRoles}} .completed-challenge( ng-show="challenge.status === 'COMPLETED' || challenge.status === 'PAST'", @@ -65,8 +66,9 @@ // Only show if not data science track p.roles span(ng-hide="challenge.track === 'DATA_SCIENCE'") - #[span Role:  ] #[span {{challenge.userDetails.roles | listRoles}}] - + span Role:   + span {{challenge.userDetails.roles | listRoles}} + .challenge.list-view(ng-show="view=='list'", ng-class="challenge.track") .active-challenge(ng-show="challenge.status === 'ACTIVE'") .challenge-track diff --git a/app/directives/external-account/external-web-links.directive.js b/app/directives/external-account/external-web-links.directive.js index aae814aea..7a985907e 100644 --- a/app/directives/external-account/external-web-links.directive.js +++ b/app/directives/external-account/external-web-links.directive.js @@ -7,9 +7,10 @@ */ angular .module('tcUIComponents') - .directive('externalWebLink', externalWebLink); + .directive('externalWebLink', ExternalWebLink); + ExternalWebLink.$inject = ['$log', 'ExternalWebLinksService']; - function externalWebLink() { + function ExternalWebLink($log, ExternalWebLinksService) { var directive = { estrict: 'E', templateUrl: 'directives/external-account/external-web-link.directive.html', @@ -17,13 +18,13 @@ linkedAccounts: '=', userHandle: '@' }, - controller: ['$log', '$scope', 'ExternalWebLinksService', ExternalWebLinkCtrl], - controllerAs: 'vm', - bindToController: true + bindToController: true, + controller: ['$log', ExternalWebLinkCtrl], + controllerAs: 'vm' }; - return directive; - function ExternalWebLinkCtrl($log, $scope, ExternalWebLinksService) { + + function ExternalWebLinkCtrl($log) { $log = $log.getInstance('ExternalWebLinkCtrl'); var vm = this; vm.addingWebLink = false; @@ -34,36 +35,20 @@ vm.addingWebLink = true; vm.errorMessage = null; ExternalWebLinksService.addLink(vm.userHandle, vm.url) - .then(function(resp) { + .then(function(data) { vm.addingWebLink = false; - $log.debug("Web link added: " + JSON.stringify(resp)); - vm.linkedAccounts.push(resp.profile); - toaster.pop('success', "Success", - String.supplant( - "Your link has been added. Data from your link will be visible on your profile shortly.", - {provider: extAccountProvider} - ) - ); + $log.debug("Web link added: " + JSON.stringify(data)); + vm.linkedAccounts.push(data); + toaster.pop('success', "Success", "Your link has been added. Data from your link will be visible on your profile shortly."); }) .catch(function(resp) { vm.addingWebLink = false; - $log.debug(JSON.stringify(resp)); - if (resp.status === 'SOCIAL_PROFILE_ALREADY_EXISTS') { - $log.info("Social profile already linked to another account"); - toaster.pop('error', "Whoops!", - String.supplant( - "This {provider} account is linked to another account. \ - If you think this is an error please contact support@apprio.com.", { - provider: extAccountProvider - } - ) - ); - } else { - $log.info("Server error:" + resp.content); - vm.errorMessage = "Sorry, we are unable add web link. If problem persist please contact support@topcoder.com"; - } + $log.error("Server error:" + resp.content); + toaster.pop('error', "Sorry, we are unable add web link. If problem persist please contact support@topcoder.com"); }); }; } + + return directive; } })(); diff --git a/app/directives/external-account/external-web-links.directive.spec.js b/app/directives/external-account/external-web-links.directive.spec.js new file mode 100644 index 000000000..41da0f479 --- /dev/null +++ b/app/directives/external-account/external-web-links.directive.spec.js @@ -0,0 +1,67 @@ +/* jshint -W117, -W030 */ +describe('ExternalWebLinks Directive', function() { + var scope = {}; + var element; + var extWebLinkSvc; + var toasterSvc; + + beforeEach(function() { + bard.appModule('topcoder'); + bard.inject(this, '$compile', '$rootScope', 'ExternalWebLinksService', '$q', 'toaster'); + + extWebLinkSvc = ExternalWebLinksService; + bard.mockService(extWebLinkSvc, { + addLink: $q.when({title: 'blah'}), + default: $q.when({}) + }); + + toasterSvc = toaster; + bard.mockService(toaster, { + pop: $q.when(true), + default: $q.when(true) + }); + + scope = $rootScope.$new(); + }); + + bard.verifyNoOutstandingHttpRequests(); + + describe('Linked external accounts', function() { + var linkedAccounts = [ + { + provider: 'github', + data: { + handle: "github-handle", + followers: 1, + publicRepos: 1 + } + } + ]; + var template, element, controller; + + beforeEach(function() { + scope.linkedAccounts = linkedAccounts; + element = angular.element(')'); + template = $compile(element)(scope); + scope.$digest(); + + controller = element.controller('externalWebLink'); + }); + + it('should have added linkedAccounts to scope', function() { + expect(scope.linkedAccounts).to.exist; + expect(scope.linkedAccounts).to.have.length(1); + }); + + it('should have added new weblink to linkedAccounts', function() { + scope.userHandle = 'test'; + scope.url = 'https://www.topcoder.com'; + controller.addWebLink(); + scope.$digest(); + expect(extWebLinkSvc.addLink).to.have.been.calledOnce; + expect(controller.linkedAccounts).to.have.length(2); + expect(toasterSvc.pop).to.have.been.calledOnce; + }) + + }); +}); diff --git a/app/services/externalAccounts.service.js b/app/services/externalAccounts.service.js index 0a03dc09b..e39ae2bf6 100644 --- a/app/services/externalAccounts.service.js +++ b/app/services/externalAccounts.service.js @@ -136,7 +136,6 @@ connection: provider, scope: "openid profile offline_access", state: callbackUrl, - // callbackURL: CONSTANTS.auth0Callback }, function(profile, idToken, accessToken, state, refreshToken) { $log.debug("onSocialLoginSuccess"); @@ -160,10 +159,16 @@ userApi.one('users', user.userId).customPOST(postData, "profiles", {}, {}) .then(function(resp) { $log.debug("Succesfully linked account: " + JSON.stringify(resp)); - resolve({ + // construct "card" object and resolve it + var _data = { status: "SUCCESS", - profile: postData - }); + linkedAccount: { + provider: provider, + data: postData + } + }; + _data.linkedAccount.data.status = 'PENDING'; + return _data; }) .catch(function(resp) { $log.error("Error linking account: " + resp.data.result.content); diff --git a/app/services/externalLinks.service.js b/app/services/externalLinks.service.js index c86d13d5d..ac9fb469b 100644 --- a/app/services/externalLinks.service.js +++ b/app/services/externalLinks.service.js @@ -27,19 +27,28 @@ links = links.plain(); if (!includePending) { _.remove(links, function(l) { - return l.status && l.status.toLowerCase() === 'pending'; + return _.get(l, 'synchronizedAt') === 0; }); } + // add provider type as weblink links = _(links).forEach(function(l) { l.provider = 'weblink'; }).value(); - // add provider type as link return links; }); } function addLink(userHandle, url) { - return memberApi.one('members', userHandle).customPOST({'url': url}, 'externalLinks'); + return memberApi.one('members', userHandle).customPOST({'url': url}, 'externalLinks') + .then(function(resp) { + debugger; + var _newLink = { + provider: 'weblink', + data: resp + }; + _newLink.data.status = 'pending'; + return _newLink; + }); } function removeLink(userHandle, key) { diff --git a/app/specs.html b/app/specs.html index a1b55ba8b..b40ef7f2d 100644 --- a/app/specs.html +++ b/app/specs.html @@ -244,32 +244,33 @@

Spec Runner

+ - + - + - + - + diff --git a/assets/css/directives/external-web-link.scss b/assets/css/directives/external-web-link.scss index 66544f358..49e382248 100644 --- a/assets/css/directives/external-web-link.scss +++ b/assets/css/directives/external-web-link.scss @@ -1,5 +1,7 @@ +@import 'topcoder-includes'; @import '../partials/combined'; + external-web-link { .web-link { margin: 6px 0 31px 0; @@ -13,7 +15,6 @@ external-web-link { display: flex; flex-flow: row wrap; - .form-label { @include sofia-pro-regular; font-size: 12px; diff --git a/tests/test-helpers/mock-data.js b/tests/test-helpers/mock-data.js index 858b3922d..80a9a5065 100644 --- a/tests/test-helpers/mock-data.js +++ b/tests/test-helpers/mock-data.js @@ -2032,7 +2032,8 @@ var mockData = (function() { "keywords": "topcoder-app,merged,oct,dashboard,15appirio-tech,20appirio-tech,polish,21appirio-tech,sup-1889,19appirio-tech", "title": "Test's profile", "images": "https://avatars3.githubusercontent.com/u/2417632?v=3&s=400,https://avatars1.githubusercontent.com/u/2417632?v=3&s=460,https://assets-cdn.github.com/images/spinners/octocat-spinner-128.gif", - "source": "embed.ly" + "source": "embed.ly", + "synchronizedAt": 123112 }, { "userId": 111, "key": "c69a1246c135b16069395010e91f5c65", @@ -2042,12 +2043,13 @@ var mockData = (function() { "keywords": "topcoder-app,merged,oct,dashboard,15appirio-tech,20appirio-tech,polish,21appirio-tech,sup-1889,19appirio-tech", "title": "Test's profile", "images": "https://avatars3.githubusercontent.com/u/2417632?v=3&s=400,https://avatars1.githubusercontent.com/u/2417632?v=3&s=460,https://assets-cdn.github.com/images/spinners/octocat-spinner-128.gif", - "source": "embed.ly" + "source": "embed.ly", + "synchronizedAt": 123123 }, { "userId": 111, "key": "c69a1246c135b16069395010e91f5c66", "handle": "test1", - "status": 'PENDING' + "synchronizedAt": 0 } ] } From 13b4020d5de6d1575c770d635f2c8f2c404d1ece Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Fri, 6 Nov 2015 09:42:08 -0800 Subject: [PATCH 04/12] SUP-2481, intergrate-web-links-api -- Added more unit tests -- Fixed existing unit tests -- Fixed UI for Adding link --- .../external-link-data.directive.jade | 6 +- .../external-web-link.directive.jade | 5 +- .../external-web-links.directive.js | 4 +- app/services/externalAccounts.service.js | 6 +- app/services/externalAccounts.service.spec.js | 120 +++++++++++++++++- app/services/externalLinks.service.js | 1 - assets/css/directives/external-web-link.scss | 28 +--- tests/test-helpers/mock-data.js | 21 +++ 8 files changed, 156 insertions(+), 35 deletions(-) diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index bb28878e6..05b158db0 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -99,6 +99,8 @@ .title {{account.data.title}} div(ng-switch-when="weblink") - .handle My Portfoilo + .handle {{account.title}} - .title {{account.data.title}} + .title {{account.description}} + + .title {{account.URL}} diff --git a/app/directives/external-account/external-web-link.directive.jade b/app/directives/external-account/external-web-link.directive.jade index 8d8478ae0..1af6331fc 100644 --- a/app/directives/external-account/external-web-link.directive.jade +++ b/app/directives/external-account/external-web-link.directive.jade @@ -8,10 +8,9 @@ p(ng-show="addWebLinkFrm.url.$error.url") Please enter a valid URL - button.tc-btn.tc-btn-secondary.tc-btn-s(type="submit", + button.tc-btn.tc-btn-m(type="submit", tc-busy-button, tc-busy-when="vm.addingWebLink", tc-busy-message="Adding", - ng-disabled="addWebLinkFrm.$invalid || addWebLinkFrm.$pristine", - ng-class="{'enabled-button': addWebLinkFrm.$valid, 'disabled': addWebLinkFrm.$pristine || addWebLinkFrm.$invalid, 'busy' : vm.addingWebLink}") Add + ng-disabled="addWebLinkFrm.$invalid || addWebLinkFrm.$pristine") Add .form-errors(ng-show="vm.errorMessage") p.form-error {{vm.errorMessage}} diff --git a/app/directives/external-account/external-web-links.directive.js b/app/directives/external-account/external-web-links.directive.js index 7a985907e..e0793017b 100644 --- a/app/directives/external-account/external-web-links.directive.js +++ b/app/directives/external-account/external-web-links.directive.js @@ -8,9 +8,9 @@ angular .module('tcUIComponents') .directive('externalWebLink', ExternalWebLink); - ExternalWebLink.$inject = ['$log', 'ExternalWebLinksService']; + ExternalWebLink.$inject = ['$log', 'ExternalWebLinksService', 'toaster']; - function ExternalWebLink($log, ExternalWebLinksService) { + function ExternalWebLink($log, ExternalWebLinksService, toaster) { var directive = { estrict: 'E', templateUrl: 'directives/external-account/external-web-link.directive.html', diff --git a/app/services/externalAccounts.service.js b/app/services/externalAccounts.service.js index dd4da169c..b6e47b6c2 100644 --- a/app/services/externalAccounts.service.js +++ b/app/services/externalAccounts.service.js @@ -44,10 +44,6 @@ .withHttpConfig({skipAuthorization: true}) .customGET('externalAccounts') .then(function(data) { - // TODO workaround for dribbble spelling mistake, remove once API is fixed - if (data.dribble) { - data.dribbble = data.dribble; - } return data; }); } @@ -166,7 +162,7 @@ } }; _data.linkedAccount.data.status = 'PENDING'; - return _data; + resolve(_data); }) .catch(function(resp) { var errorStatus = "FATAL_ERROR"; diff --git a/app/services/externalAccounts.service.spec.js b/app/services/externalAccounts.service.spec.js index 408f89f4c..f8a15e241 100644 --- a/app/services/externalAccounts.service.spec.js +++ b/app/services/externalAccounts.service.spec.js @@ -3,12 +3,16 @@ describe('ExternalAccount Service', function() { var service; var mockAccountsData = mockData.getMockLinkedExternalAccountsData(); var mockUserLinksData = mockData.getMockLinkedExternalAccounts(); + var mockAuth0Profile = mockData.getMockAuth0Profile(); + var mockProfile = mockData.getMockProfile(); var apiUrl; + var auth0, userService; + var profilePost, profileDelete; beforeEach(function() { bard.appModule('topcoder'); - bard.inject(this, 'ExternalAccountService', '$httpBackend', '$q', 'CONSTANTS', 'JwtInterceptorService'); + bard.inject(this, 'ExternalAccountService', '$httpBackend', '$q', 'CONSTANTS', 'JwtInterceptorService', 'auth', 'UserService'); bard.mockService(JwtInterceptorService, { getToken: $q.when('token'), _default: $q.when([]) @@ -16,6 +20,28 @@ describe('ExternalAccount Service', function() { apiUrl = CONSTANTS.API_URL; service = ExternalAccountService; + auth0 = auth; + userService = UserService; + + // mock user api + sinon.stub(auth0, 'signin', function(params, successCallback, failureCallback) { + if (params && params.state == 'failure') { + failureCallback.call(failureCallback, "MOCK_ERROR"); + } + successCallback.call( + successCallback, + mockAuth0Profile, + "mockAuth0IdToken", + "mockAuth0AccessToken", + params.state, + null + ); + }); + + // mock user service + sinon.stub(userService, 'getUserIdentity', function() { + return {userId: 111, handle: mockProfile.handle }; + }); $httpBackend .when('GET', apiUrl + '/members/test1/externalAccounts/') @@ -23,6 +49,11 @@ describe('ExternalAccount Service', function() { $httpBackend .when('GET', apiUrl + '/users/111/?fields=profiles') .respond(200, {result: {content: mockUserLinksData}}); + profilePost = $httpBackend.when('POST', apiUrl + '/users/111/profiles/'); + profilePost.respond(200, {result: {content: mockProfile}}); + + profileDelete = $httpBackend.when('DELETE', apiUrl + '/users/111/profiles/stackoverflow/'); + profileDelete.respond(200, {result: {content: mockProfile}}); }); @@ -73,4 +104,91 @@ describe('ExternalAccount Service', function() { $httpBackend.flush(); }); + it('should link external account', function() { + // call linkExternalAccount method with supporte network, should succeed + service.linkExternalAccount('stackoverflow', "callback").then(function(data) { + expect(data).to.be.defined; + // console.log(data); + expect(data.status).to.exist.to.equal('SUCCESS'); + expect(data.linkedAccount).to.exist; + expect(data.linkedAccount.provider).to.exist.to.equal('stackoverflow'); + expect(data.linkedAccount.data).to.exist; + expect(data.linkedAccount.data.status).to.exist.to.equal('PENDING'); + }); + $httpBackend.flush(); + }); + + it('should fail with unsupported network', function() { + // call linkExternalAccount method with unsupported network, should fail + service.linkExternalAccount('unsupported', "callback").then(function(data) { + expect(data).to.be.defined; + expect(data.status).to.exist.to.equal('failed'); + expect(data.error.to.contain('unsupported')); + }); + }); + + it('should fail with already existing profile', function() { + var errorMessage = "social profile exists"; + profilePost.respond(400, {result: { status: 400, content: errorMessage } }); + // call linkExternalAccount method, having user service throw already exist + service.linkExternalAccount('stackoverflow', "callback").then(function(data) { + sinon.assert.fail('should not be called'); + }, function(error) { + expect(error).to.be.defined; + expect(error.status).to.exist.to.equal('SOCIAL_PROFILE_ALREADY_EXISTS'); + expect(error.msg).to.exist.to.equal(errorMessage); + }); + $httpBackend.flush(); + }); + + it('should fail with auth0 error', function() { + // call linkExternalAccount method with auth0 throwing error + service.linkExternalAccount('stackoverflow', "failure").then(function(data) { + sinon.assert.fail('should not be called'); + }, function(error) { + expect(error).to.be.exist.to.equal('MOCK_ERROR'); + }); + $httpBackend.flush(); + }); + + it('should unlink external account', function() { + var errorMessage = "social profile exists"; + profilePost.respond(400, {result: { status: 400, content: errorMessage } }); + // call unlinkExternalAccount method with supporte network, should succeed + service.unlinkExternalAccount('stackoverflow').then(function(data) { + expect(data).to.be.defined; + // console.log(data); + expect(data.status).to.exist.to.equal('SUCCESS'); + }); + $httpBackend.flush(); + }); + + it('should fail, with profile does not exist, in unlinking external account', function() { + var errorMessage = "social profile does not exists"; + profileDelete.respond(404, {result: { status: 404, content: errorMessage } }); + // call unlinkExternalAccount method with supporte network, should succeed + service.unlinkExternalAccount('stackoverflow').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('SOCIAL_PROFILE_NOT_EXIST'); + expect(error.msg).to.exist.to.equal(errorMessage); + }); + $httpBackend.flush(); + }); + + it('should fail, with fatal error, in unlinking external account', function() { + var errorMessage = "bad request"; + profileDelete.respond(400, {result: { status: 400, content: errorMessage } }); + // call unlinkExternalAccount method with supporte network, should succeed + service.unlinkExternalAccount('stackoverflow').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/services/externalLinks.service.js b/app/services/externalLinks.service.js index ac9fb469b..ce1094854 100644 --- a/app/services/externalLinks.service.js +++ b/app/services/externalLinks.service.js @@ -41,7 +41,6 @@ function addLink(userHandle, url) { return memberApi.one('members', userHandle).customPOST({'url': url}, 'externalLinks') .then(function(resp) { - debugger; var _newLink = { provider: 'weblink', data: resp diff --git a/assets/css/directives/external-web-link.scss b/assets/css/directives/external-web-link.scss index 49e382248..6c8811045 100644 --- a/assets/css/directives/external-web-link.scss +++ b/assets/css/directives/external-web-link.scss @@ -1,6 +1,4 @@ -@import 'topcoder-includes'; -@import '../partials/combined'; - +@import 'tc-includes'; external-web-link { .web-link { @@ -33,9 +31,10 @@ external-web-link { .form-field-focused { @include form-field-focused; } - + .validation-bar.url { flex: 1; + width: auto; &:before { height: 40px; @@ -47,23 +46,10 @@ external-web-link { margin: 0px; } - // button { - // @include button-2-m; - // font-size: 14px; - // line-height: 16px; - // height: 40px; - // width: 60px; - // margin: 0 0 0 10px; - // text-transform: uppercase; - - // &.enabled-button { - // @include ui-enabled-button; - // } - - // &.busy { - // width: 94px; - // } - // } + button { + width: auto; + margin-left: 10px; + } } } } diff --git a/tests/test-helpers/mock-data.js b/tests/test-helpers/mock-data.js index 80a9a5065..caf8c1d26 100644 --- a/tests/test-helpers/mock-data.js +++ b/tests/test-helpers/mock-data.js @@ -24,6 +24,7 @@ var mockData = (function() { getMockLinkedExternalAccounts: getMockLinkedExternalAccounts, getMockLinkedExternalAccountsData: getMockLinkedExternalAccountsData, getMockExternalWebLinksData: getMockExternalWebLinksData, + getMockAuth0Profile: getMockAuth0Profile }; function getMockStates() { @@ -2054,4 +2055,24 @@ var mockData = (function() { ] } + function getMockAuth0Profile() { + return { + "user_id": "mockSocialNetwork|123456", + "given_name": "mock", + "family_name": "user", + "first_name": "mock", + "last_name": "user", + "nickname": "mocky", + "name": "mock user", + "email": "mock@topcoder.com", + "username": "mockuser", + "identities": [ + { + "access_token": "abcdefghi", + "access_token_secret": "abcdefghijklmnopqrstuvwxyz" + } + ] + }; + } + })(); From a1df4e4b483cae6c729eb20a0c5a59b50891790c Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 10 Nov 2015 14:30:24 +0530 Subject: [PATCH 05/12] SUP-2481, intergrate-web-links-api -- More unit tests -- Fixed removeLink method in externalLinks.service.js --- app/services/externalAccounts.service.spec.js | 50 +++++++++++++++++-- app/services/externalLinks.service.js | 2 +- app/services/externalLinks.service.spec.js | 40 +++++++++++++-- 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/app/services/externalAccounts.service.spec.js b/app/services/externalAccounts.service.spec.js index f8a15e241..39a8af578 100644 --- a/app/services/externalAccounts.service.spec.js +++ b/app/services/externalAccounts.service.spec.js @@ -7,7 +7,7 @@ describe('ExternalAccount Service', function() { var mockProfile = mockData.getMockProfile(); var apiUrl; var auth0, userService; - var profilePost, profileDelete; + var profileGet, profilePost, profileDelete; beforeEach(function() { @@ -46,9 +46,10 @@ describe('ExternalAccount Service', function() { $httpBackend .when('GET', apiUrl + '/members/test1/externalAccounts/') .respond(200, {result: {content: mockAccountsData}}); - $httpBackend - .when('GET', apiUrl + '/users/111/?fields=profiles') - .respond(200, {result: {content: mockUserLinksData}}); + + profileGet = $httpBackend.when('GET', apiUrl + '/users/111/?fields=profiles'); + profileGet.respond(200, {result: {content: mockUserLinksData}}); + profilePost = $httpBackend.when('POST', apiUrl + '/users/111/profiles/'); profilePost.respond(200, {result: {content: mockProfile}}); @@ -104,6 +105,33 @@ describe('ExternalAccount Service', function() { $httpBackend.flush(); }); + it('should not return unsupported links even if they are returned by the API', function() { + var profiles = JSON.parse(JSON.stringify(mockUserLinksData)); + profiles.profiles.push({providerType: 'unsupported'}); + profileGet.respond(200, {result: {content: profiles}}); + // spy + service.getAllExternalLinks('test1', 111, true).then(function(data) { + expect(data).to.be.defined; + expect(_.pluck(data, 'provider')).to.include.members(['dribbble', 'github','bitbucket', 'stackoverflow']); + expect(_.all(_.pluck(data, 'data'))).to.be.truthy; + }); + $httpBackend.flush(); + }); + + it('should fail in returning links', function() { + var errorMessage = "bad request"; + // mocks the GET call to respond with 400 bad request + profileGet.respond(400, {result: { status: 400, content: errorMessage } }); + // calls getAllExternalLinks method with valid params + service.getAllExternalLinks('test1', 111, true).then(function(data) { + sinon.assert.fail('should not be called'); + }).catch(function(resp) { + expect(resp).to.exist; + expect(resp.status).to.exist.to.equal(400); + }); + $httpBackend.flush(); + }); + it('should link external account', function() { // call linkExternalAccount method with supporte network, should succeed service.linkExternalAccount('stackoverflow', "callback").then(function(data) { @@ -151,6 +179,20 @@ describe('ExternalAccount Service', function() { $httpBackend.flush(); }); + it('should fail, with fatal error, in linking external account', function() { + var errorMessage = "endpoint not found"; + profilePost.respond(404, {result: { status: 404, content: errorMessage } }); + // call unlinkExternalAccount method with supporte network, should succeed + service.linkExternalAccount('stackoverflow', "callback").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 unlink external account', function() { var errorMessage = "social profile exists"; profilePost.respond(400, {result: { status: 400, content: errorMessage } }); diff --git a/app/services/externalLinks.service.js b/app/services/externalLinks.service.js index ce1094854..02327c3d6 100644 --- a/app/services/externalLinks.service.js +++ b/app/services/externalLinks.service.js @@ -51,7 +51,7 @@ } function removeLink(userHandle, key) { - return memberApi.one('members', userHandle).one('externalLinks', key).delete(); + return memberApi.one('members', userHandle).one('externalLinks', key).remove(); } } diff --git a/app/services/externalLinks.service.spec.js b/app/services/externalLinks.service.spec.js index e6fb1723e..0bbca515c 100644 --- a/app/services/externalLinks.service.spec.js +++ b/app/services/externalLinks.service.spec.js @@ -3,19 +3,31 @@ describe('ExternalWebLinks service', function() { var service; var mockExternalLinks = mockData.getMockExternalWebLinksData(); var apiUrl; + var linksGet, linksPost, linksDelete; beforeEach(function() { bard.appModule('topcoder'); - bard.inject(this, 'ExternalWebLinksService', '$httpBackend', 'CONSTANTS'); + bard.inject(this, 'ExternalWebLinksService', 'JwtInterceptorService', '$httpBackend', 'CONSTANTS', '$q'); + bard.mockService(JwtInterceptorService, { + getToken: $q.when('token'), + _default: $q.when([]) + }); apiUrl = CONSTANTS.API_URL; service = ExternalWebLinksService; // mock profile api - $httpBackend - .when('GET', apiUrl + '/members/test1/externalLinks/') - .respond(200, {result: {content: mockExternalLinks}}); + linksGet = $httpBackend.when('GET', apiUrl + '/members/test1/externalLinks/'); + linksGet.respond(200, {result: {content: mockExternalLinks}}); + + // mock profile api [POST] + linksPost = $httpBackend.when('POST', apiUrl + '/members/test1/externalLinks/'); + linksPost.respond(200, {result: {content: mockExternalLinks[0]}}); + + // mock profile api [DELETE] + linksDelete = $httpBackend.when('DELETE', apiUrl + '/members/test1/externalLinks/testkey/'); + linksDelete.respond(200, {result: {}}); }); afterEach(function() { @@ -41,4 +53,24 @@ describe('ExternalWebLinks service', function() { $httpBackend.flush(); }); + it('should add external link', function() { + // call addLink method with valid params, should succeed + service.addLink('test1', "http://google.com").then(function(newLink) { + expect(newLink).to.be.exist; + expect(newLink.provider).to.exist.to.equal('weblink'); + expect(newLink.data).to.exist; + expect(newLink.data.status).to.exist.to.equal('pending'); + }); + $httpBackend.flush(); + }); + + it('should remove external link', function() { + // call removeLink method with valid params, should succeed + service.removeLink('test1', "testkey").then(function(newLink) { + }).catch(function(error) { + sinon.assert.fail('should not be called'); + }); + $httpBackend.flush(); + }); + }); From 34eb3826daaf2a6ed5b57e5817d2e883024c52e9 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2015 12:35:20 +0530 Subject: [PATCH 06/12] SUP-2481, Integrate w/ API for External web links -- Removed description field from the card. --- .../external-account/external-link-data.directive.jade | 2 -- 1 file changed, 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 05b158db0..f6c35f490 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -101,6 +101,4 @@ div(ng-switch-when="weblink") .handle {{account.title}} - .title {{account.description}} - .title {{account.URL}} From 8e7941b387fd118bc2b81cc1815f32e2659a9291 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2015 15:23:44 +0530 Subject: [PATCH 07/12] SUP-2481, intergrate-web-links-api -- Ellipsis the text for title field in the data card. Unable to do the same for hyperlink (URL field) because the angular-ellipsis is not working for it. --- .../external-link-data.directive.jade | 4 +-- app/index.jade | 1 + app/topcoder.module.js | 3 +- assets/css/directives/external-link-data.scss | 28 +++++++++++++++++++ bower.json | 1 + 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index f6c35f490..158a5eae7 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -99,6 +99,6 @@ .title {{account.data.title}} div(ng-switch-when="weblink") - .handle {{account.title}} + p.link-title(data-ellipsis, ng-bind="account.title") - .title {{account.URL}} + a.link-url(ng-href="{{account.URL}}", ng-bind="account.URL") diff --git a/app/index.jade b/app/index.jade index caaca0bda..65b8274b9 100644 --- a/app/index.jade +++ b/app/index.jade @@ -129,6 +129,7 @@ html script(src='../bower_components/angucomplete-alt/angucomplete-alt.js') script(src='../bower_components/angular-cookies/angular-cookies.js') script(src='../bower_components/angular-dropdowns/dist/angular-dropdowns.js') + script(src='../bower_components/angular-ellipsis/src/angular-ellipsis.js') script(src='../bower_components/angular-filter/dist/angular-filter.min.js') script(src='../bower_components/angular-img-fallback/angular.dcb-img-fallback.js') script(src='../bower_components/intro.js/intro.js') diff --git a/app/topcoder.module.js b/app/topcoder.module.js index 381202abb..fcb692b88 100644 --- a/app/topcoder.module.js +++ b/app/topcoder.module.js @@ -33,7 +33,8 @@ 'angular-intro', 'ngMessages', 'angular-carousel', - 'sticky' + 'sticky', + 'dibari.angular-ellipsis' ]; angular.module('topcoder', dependencies).run(appRun); diff --git a/assets/css/directives/external-link-data.scss b/assets/css/directives/external-link-data.scss index 3c068ce80..76a3f1eb6 100644 --- a/assets/css/directives/external-link-data.scss +++ b/assets/css/directives/external-link-data.scss @@ -117,6 +117,29 @@ external-accounts { padding: 10px; font-size: 50px; } + + .link-title { + @include sofia-pro-medium; + font-size: 12px; + line-height: 20px; + // to handle the missing title scenario + min-height: 20px; + color: $gray-darkest; + margin-top: 20px; + max-height: 60px; + overflow: hidden; + padding: 0px 20px; + } + + .link-url { + font-size: 12px; + line-height: 14px; + word-wrap: break-word; + display: block; + overflow: hidden; + max-height: 28px; + padding: 0px 20px; + } } } @@ -213,6 +236,11 @@ external-accounts { .logo { padding: 10px; font-size: 50px; + } + + .link-title { + margin-top: 20px; + margin-bottom: 20px; } } } diff --git a/bower.json b/bower.json index 8d30fdc72..424e61c59 100644 --- a/bower.json +++ b/bower.json @@ -28,6 +28,7 @@ "angular": "1.4.x", "angular-cookies": "1.4.x", "angular-dropdowns": "1.1.0", + "angular-ellipsis": "~0.1.6", "angular-filter": "~0.5.4", "angular-img-fallback": "~0.1.3", "angular-intro.js": "~1.3.0", From 39c198a107568b6c8c79234374131d6ace049929 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2015 17:28:41 +0530 Subject: [PATCH 08/12] SUP-2481, intergrate-web-links-api -- Fixed determination of pending status for an external account. It was using wrong field to set pending status. Came across this issue while debugging for SUP-2479 -- Fixed existing unit tests and added new for pending status fix --- .../external-account.directive.js | 5 ++- .../external-account.directive.spec.js | 39 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/directives/external-account/external-account.directive.js b/app/directives/external-account/external-account.directive.js index d1f0dc522..affbbc92d 100644 --- a/app/directives/external-account/external-account.directive.js +++ b/app/directives/external-account/external-account.directive.js @@ -30,14 +30,15 @@ var _accountList = _.clone(_supportedAccounts, true); _.remove(_accountList, function(al) { return al.order < 0}); $scope.$watchCollection('linkedAccounts', function(newValue, oldValue) { - if (newValue && newValue != oldValue) { + if (newValue) { angular.forEach(_accountList, function(account) { var _linkedAccount = _.find(newValue, function(p) { return p.provider === account.provider }); + var accountStatus = _.get(_linkedAccount, 'data.status', null); if (!_linkedAccount) { account.status = 'unlinked'; - } else if(_linkedAccount.status && _linkedAccount.status.toLowerCase() === 'pending') { + } else if(accountStatus && accountStatus.toLowerCase() === 'pending') { account.status = 'pending'; } else { account.status = 'linked'; diff --git a/app/directives/external-account/external-account.directive.spec.js b/app/directives/external-account/external-account.directive.spec.js index 634d49257..a7111f090 100644 --- a/app/directives/external-account/external-account.directive.spec.js +++ b/app/directives/external-account/external-account.directive.spec.js @@ -15,10 +15,15 @@ describe('External Accounts Directive', function() { var linkedAccounts = [ { provider: 'linkedin', - // don't care about other details + data: { + // don't care about other details + } }, { - provider: 'github' + provider: 'github', + data: { + // don't care about other details + } } ]; var externalAccounts; @@ -27,31 +32,27 @@ describe('External Accounts Directive', function() { scope.linkedAccounts = linkedAccounts; element = angular.element(')'); externalAccounts = $compile(element)(scope); - // externalAccounts.scope().$digest(); - // externalAccounts.scope().$apply(); + scope.$digest(); }); it('should have added account list to scope', function() { - inject(function($timeout) { - scope.$digest(); - $timeout(function() { - expect(element.isolateScope().accountList).to.exist; - }, 0) - }); + expect(element.isolateScope().accountList).to.exist; }); it('should have "linked" property set for github & linkedin', function() { - inject(function($timeout) { - scope.$digest(); - $timeout(function() { - var githubAccount = _.find(element.isolateScope().accountList, function(a) { return a.provider === 'github'}); - expect(githubAccount).to.have.property('status').that.equals('linked'); - }, 0) + var githubAccount = _.find(element.isolateScope().accountList, function(a) { + return a.provider === 'github' }); + expect(githubAccount).to.have.property('status').that.equals('linked'); + }); - // var linkeindAccount = _.find(element.isolateScope().accountList, function(a) { return a.provider === 'linkedin'}); - // expect(linkeindAccount).to.have.property('linked') - // .that.equals(true); + it('should have pending status for stackoverflow ', function() { + scope.linkedAccounts.push({provider: 'stackoverflow', data: {status: 'PENDING'}}); + scope.$digest(); + var soAccount = _.find(element.isolateScope().accountList, function(a) { + return a.provider === 'stackoverflow' + }); + expect(soAccount).to.have.property('status').that.equals('pending'); }); }); }); From 92d84b3fcc3d09bbcb34cb77f793bac2320271f3 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 17 Nov 2015 09:52:13 +0530 Subject: [PATCH 09/12] SUP-2481, intergrate-web-links-api -- Code review changes --- app/directives/external-account/external-account.directive.js | 2 -- app/directives/external-account/external-web-links.directive.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/directives/external-account/external-account.directive.js b/app/directives/external-account/external-account.directive.js index affbbc92d..143893e7c 100644 --- a/app/directives/external-account/external-account.directive.js +++ b/app/directives/external-account/external-account.directive.js @@ -19,7 +19,6 @@ templateUrl: 'directives/external-account/external-account.directive.html', scope: { linkedAccounts: '=', - // linksData: '=', readOnly: '=' }, controller: ['$log', '$scope', 'ExternalAccountService', 'toaster', @@ -99,7 +98,6 @@ }); // remove from both links array and links data array $scope.linkedAccounts.splice(toRemove, 1); - // delete $scope.linksData[provider.provider]; toaster.pop('success', "Success", String.supplant( "Your {provider} account has been unlinked.", diff --git a/app/directives/external-account/external-web-links.directive.js b/app/directives/external-account/external-web-links.directive.js index e0793017b..5413ebcf0 100644 --- a/app/directives/external-account/external-web-links.directive.js +++ b/app/directives/external-account/external-web-links.directive.js @@ -12,7 +12,7 @@ function ExternalWebLink($log, ExternalWebLinksService, toaster) { var directive = { - estrict: 'E', + restrict: 'E', templateUrl: 'directives/external-account/external-web-link.directive.html', scope: { linkedAccounts: '=', From 9692e5fb20c3a6ce4fe7c07228bbc97bbdd8ddd6 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 17 Nov 2015 10:12:04 +0530 Subject: [PATCH 10/12] SUP-2481, intergrate-web-links-api -- Fixed NPE when a new external account is added. --- app/directives/external-account/external-account.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/directives/external-account/external-account.directive.js b/app/directives/external-account/external-account.directive.js index 143893e7c..16fa17ac7 100644 --- a/app/directives/external-account/external-account.directive.js +++ b/app/directives/external-account/external-account.directive.js @@ -64,7 +64,7 @@ ExternalAccountService.linkExternalAccount(provider.provider, null) .then(function(resp) { $log.debug("Social account linked: " + JSON.stringify(resp)); - $scope.linkedAccounts.push(resp.profile); + $scope.linkedAccounts.push(resp.linkedAccount); toaster.pop('success', "Success", String.supplant( "Your {provider} account has been linked. Data from your linked account will be visible on your profile shortly.", From 6167b0b8934d5fc778200c2ccb33dacaf9cefb6c Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Wed, 18 Nov 2015 17:59:23 +0530 Subject: [PATCH 11/12] SUP-2481, intergrate-web-links-api -- More tests for external account directive (now it includes link and unlink method too) --- .../external-account.directive.js | 11 +- .../external-account.directive.spec.js | 242 ++++++++++++++++-- 2 files changed, 235 insertions(+), 18 deletions(-) diff --git a/app/directives/external-account/external-account.directive.js b/app/directives/external-account/external-account.directive.js index 16fa17ac7..162f232a9 100644 --- a/app/directives/external-account/external-account.directive.js +++ b/app/directives/external-account/external-account.directive.js @@ -44,6 +44,11 @@ } }); $scope.accountList = _accountList; + } else { + // reset the status for all accounts + angular.forEach(_accountList, function(account) { + delete account.status; + }); } }); @@ -96,8 +101,10 @@ var toRemove = _.findIndex($scope.linkedAccounts, function(la) { return la.provider === provider.provider; }); - // remove from both links array and links data array - $scope.linkedAccounts.splice(toRemove, 1); + if (toRemove > -1) { + // remove from the linkedAccounts array + $scope.linkedAccounts.splice(toRemove, 1); + } toaster.pop('success', "Success", String.supplant( "Your {provider} account has been unlinked.", diff --git a/app/directives/external-account/external-account.directive.spec.js b/app/directives/external-account/external-account.directive.spec.js index a7111f090..4d512f131 100644 --- a/app/directives/external-account/external-account.directive.spec.js +++ b/app/directives/external-account/external-account.directive.spec.js @@ -2,37 +2,101 @@ describe('External Accounts Directive', function() { var scope; var element; + var toasterSvc; + var extAccountSvc; + var mockLinkedAccounts = [ + { + provider: 'linkedin', + data: { + // don't care about other details + } + }, + { + provider: 'github', + data: { + // don't care about other details + } + } + ]; beforeEach(function() { bard.appModule('topcoder'); - bard.inject(this, '$compile', '$rootScope'); + bard.inject(this, '$compile', '$rootScope', 'toaster', 'ExternalAccountService', '$q'); + + extAccountSvc = ExternalAccountService; + // mock external account service + sinon.stub(extAccountSvc, 'linkExternalAccount', function(provider) { + var $deferred = $q.defer(); + if (provider === 'twitter') { + $deferred.reject({ + status: 'SOCIAL_PROFILE_ALREADY_EXISTS', + msg: 'profile already exists' + }); + } else if(provider === 'weblink') { + $deferred.reject({ + status: 'FATAL_ERROR', + msg: 'fatal error' + }); + } else { + $deferred.resolve({ + status: 'SUCCESS', + linkedAccount : { + data: { + status: 'PENDING' + }, + provider: provider + } + }); + } + return $deferred.promise; + }); + sinon.stub(extAccountSvc, 'unlinkExternalAccount', function(provider) { + var $deferred = $q.defer(); + if (provider === 'twitter') { + $deferred.reject({ + status: 'SOCIAL_PROFILE_NOT_EXIST', + msg: 'profile not exists' + }); + } else if(provider === 'weblink') { + $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) + }); + scope = $rootScope.$new(); }); bard.verifyNoOutstandingHttpRequests(); describe('Linked external accounts', function() { - var linkedAccounts = [ - { - provider: 'linkedin', - data: { - // don't care about other details - } - }, - { - provider: 'github', - data: { - // don't care about other details - } - } - ]; - var externalAccounts; + var linkedAccounts = angular.copy(mockLinkedAccounts); + var externalAccounts, controller; beforeEach(function() { scope.linkedAccounts = linkedAccounts; element = angular.element(')'); externalAccounts = $compile(element)(scope); scope.$digest(); + + controller = element.controller('externalAccounts'); + }); + + afterEach(function() { + linkedAccounts = angular.copy(mockLinkedAccounts); + scope.linkedAccounts = linkedAccounts; }); it('should have added account list to scope', function() { @@ -54,5 +118,151 @@ describe('External Accounts Directive', function() { }); expect(soAccount).to.have.property('status').that.equals('pending'); }); + + it('should reset accountList when linkedAccounts set to null ', function() { + scope.linkedAccounts = null; + scope.$digest(); + expect(element.isolateScope().accountList).to.have.length(7); + expect(_.all(_.pluck(element.isolateScope().accountList, 'status'))).to.be.false; + }); + + it('should link external account ', function() { + element.isolateScope().handleClick('stackoverflow', 'unlinked'); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; + expect(element.isolateScope().linkedAccounts).to.have.length(3); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['github', 'linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else if (['stackoverflow'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('pending'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + + it('should NOT link external account with fatal error ', function() { + element.isolateScope().handleClick('weblink', 'unlinked'); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('error').calledOnce; + expect(element.isolateScope().linkedAccounts).to.have.length(2); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['github', 'linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + + it('should NOT link external account with already existing account ', function() { + element.isolateScope().handleClick('twitter', 'unlinked'); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('error').calledOnce; + expect(element.isolateScope().linkedAccounts).to.have.length(2); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['github', 'linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + + it('should unlink external account ', function() { + element.isolateScope().handleClick('github', 'linked'); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; + expect(element.isolateScope().linkedAccounts).to.have.length(1); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + + it('should unlink if controller doesn\'t have account linked but API returns success ', function() { + element.isolateScope().handleClick('stackoverflow', 'linked'); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; + expect(element.isolateScope().linkedAccounts).to.have.length(2); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['github', 'linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + + it('should NOT ulink external account with fatal error ', function() { + element.isolateScope().handleClick('weblink', 'linked'); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('error').calledOnce; + expect(element.isolateScope().linkedAccounts).to.have.length(2); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['github', 'linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + + it('should NOT unlink external account with already unlinked account ', function() { + element.isolateScope().handleClick('twitter', 'linked'); + scope.$digest(); + expect(toasterSvc.pop).to.have.been.calledWith('error').calledOnce; + expect(element.isolateScope().linkedAccounts).to.have.length(2); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['github', 'linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + + it('should not do anything ', function() { + element.isolateScope().handleClick('github', 'pending'); + scope.$digest(); + expect(toasterSvc.pop).to.have.callCount(0); + expect(element.isolateScope().linkedAccounts).to.have.length(2); + expect(element.isolateScope().accountList).to.have.length(7); + element.isolateScope().accountList.forEach(function(account) { + expect(account.status).to.exist; + expect(account.provider).to.exist; + if (['github', 'linkedin'].indexOf(account.provider) != -1) { + expect(account.status).to.equal('linked'); + } else { + expect(account.status).to.equal('unlinked'); + } + }); + }); + }); }); From fda6af178f33a7ea2ab6c60d97a745c80848f656 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Tue, 24 Nov 2015 10:54:27 +0530 Subject: [PATCH 12/12] SUP-2481, intergrate-web-links-api -- Fixed CSS for mobile and desktop view -- Fixed issue where added link was not visible immediately after addition -- Handled already added link error --- .../external-link-data.directive.jade | 3 +- .../external-web-link.directive.jade | 10 +-- .../external-web-links.directive.js | 41 +++++---- .../external-web-links.directive.spec.js | 84 +++++++++++++++---- app/services/externalLinks.service.js | 26 ++++-- app/services/externalLinks.service.spec.js | 16 +++- .../edit-profile/edit-profile.controller.js | 11 --- app/settings/edit-profile/edit-profile.jade | 2 +- assets/css/directives/external-link-data.scss | 24 ++++-- 9 files changed, 150 insertions(+), 67 deletions(-) diff --git a/app/directives/external-account/external-link-data.directive.jade b/app/directives/external-account/external-link-data.directive.jade index 158a5eae7..77a486ffe 100644 --- a/app/directives/external-account/external-link-data.directive.jade +++ b/app/directives/external-account/external-link-data.directive.jade @@ -99,6 +99,7 @@ .title {{account.data.title}} div(ng-switch-when="weblink") - p.link-title(data-ellipsis, ng-bind="account.title") + 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. a.link-url(ng-href="{{account.URL}}", ng-bind="account.URL") diff --git a/app/directives/external-account/external-web-link.directive.jade b/app/directives/external-account/external-web-link.directive.jade index 1af6331fc..0f99e4cec 100644 --- a/app/directives/external-account/external-web-link.directive.jade +++ b/app/directives/external-account/external-web-link.directive.jade @@ -1,7 +1,7 @@ .web-link - form(name="addWebLinkFrm", ng-submit="addWebLinkFrm.$valid && vm.addWebLink()", autocomplete="off") + form(name="addWebLinkFrm", ng-submit="addWebLinkFrm.$valid && addWebLink()", autocomplete="off") .validation-bar.url(ng-class="{ 'error-bar': (addWebLinkFrm.url.$dirty && addWebLinkFrm.url.$invalid) }") - input.form-field.url(name="url", type="url", ng-model="vm.url", placeholder="http://www.yourlink.com", required) + input.form-field.url(name="url", type="url", ng-model="url", placeholder="http://www.yourlink.com", required) .form-input-error(ng-show="addWebLinkFrm.url.$dirty && addWebLinkFrm.url.$invalid") p(ng-show="addWebLinkFrm.url.$error.required") This is a required field. @@ -9,8 +9,8 @@ p(ng-show="addWebLinkFrm.url.$error.url") Please enter a valid URL button.tc-btn.tc-btn-m(type="submit", - tc-busy-button, tc-busy-when="vm.addingWebLink", tc-busy-message="Adding", + tc-busy-button, tc-busy-when="addingWebLink", tc-busy-message="Adding", ng-disabled="addWebLinkFrm.$invalid || addWebLinkFrm.$pristine") Add - .form-errors(ng-show="vm.errorMessage") - p.form-error {{vm.errorMessage}} + .form-errors(ng-show="errorMessage") + p.form-error {{errorMessage}} diff --git a/app/directives/external-account/external-web-links.directive.js b/app/directives/external-account/external-web-links.directive.js index 5413ebcf0..411d1f22f 100644 --- a/app/directives/external-account/external-web-links.directive.js +++ b/app/directives/external-account/external-web-links.directive.js @@ -18,33 +18,40 @@ linkedAccounts: '=', userHandle: '@' }, - bindToController: true, - controller: ['$log', ExternalWebLinkCtrl], - controllerAs: 'vm' + controller: ['$scope', '$log', ExternalWebLinkCtrl] }; - function ExternalWebLinkCtrl($log) { + function ExternalWebLinkCtrl($scope, $log) { $log = $log.getInstance('ExternalWebLinkCtrl'); - var vm = this; - vm.addingWebLink = false; - vm.errorMessage = null; + $scope.addingWebLink = false; + $scope.errorMessage = null; - vm.addWebLink = function() { - $log.debug("URL: " + vm.url); - vm.addingWebLink = true; - vm.errorMessage = null; - ExternalWebLinksService.addLink(vm.userHandle, vm.url) + $scope.addWebLink = function() { + $log.debug("URL: " + $scope.url); + $scope.addingWebLink = true; + $scope.errorMessage = null; + ExternalWebLinksService.addLink($scope.userHandle, $scope.url) .then(function(data) { - vm.addingWebLink = false; + $scope.addingWebLink = false; $log.debug("Web link added: " + JSON.stringify(data)); - vm.linkedAccounts.push(data); + data.data.provider = data.provider; + $scope.linkedAccounts.push(data.data); toaster.pop('success', "Success", "Your link has been added. Data from your link will be visible on your profile shortly."); }) .catch(function(resp) { - vm.addingWebLink = false; - $log.error("Server error:" + resp.content); - toaster.pop('error', "Sorry, we are unable add web link. If problem persist please contact support@topcoder.com"); + $scope.addingWebLink = false; + + if (resp.status === 'WEBLINK_ALREADY_EXISTS') { + $log.info("Social profile already linked to another account"); + toaster.pop('error', "Whoops!", + "This weblink is already added to your account. \ + If you think this is an error please contact support@topcoder.com." + ); + } else { + $log.error("Fatal Error: addWebLink: " + resp.msg); + toaster.pop('error', "Sorry, we are unable add web link. If problem persist please contact support@topcoder.com."); + } }); }; } diff --git a/app/directives/external-account/external-web-links.directive.spec.js b/app/directives/external-account/external-web-links.directive.spec.js index 41da0f479..6e8997e8b 100644 --- a/app/directives/external-account/external-web-links.directive.spec.js +++ b/app/directives/external-account/external-web-links.directive.spec.js @@ -4,15 +4,45 @@ describe('ExternalWebLinks Directive', function() { var element; var extWebLinkSvc; var toasterSvc; + var mockLinkedAccounts = [ + { + provider: 'github', + data: { + handle: "github-handle", + followers: 1, + publicRepos: 1 + } + } + ]; beforeEach(function() { bard.appModule('topcoder'); bard.inject(this, '$compile', '$rootScope', 'ExternalWebLinksService', '$q', 'toaster'); extWebLinkSvc = ExternalWebLinksService; - bard.mockService(extWebLinkSvc, { - addLink: $q.when({title: 'blah'}), - default: $q.when({}) + + // mock external weblink service + sinon.stub(extWebLinkSvc, 'addLink', function(handle, url) { + var $deferred = $q.defer(); + if (handle === 'throwError') { + $deferred.reject({ + status: 'FATAL_ERROR', + msg: 'fatal error' + }); + } else if(handle === 'alreadyExistsError') { + $deferred.reject({ + status: 'WEBLINK_ALREADY_EXISTS', + msg: 'link already added to your account' + }); + } else { + $deferred.resolve({ + data: { + status: 'PENDING' + }, + provider: 'weblink' + }); + } + return $deferred.promise; }); toasterSvc = toaster; @@ -27,16 +57,7 @@ describe('ExternalWebLinks Directive', function() { bard.verifyNoOutstandingHttpRequests(); describe('Linked external accounts', function() { - var linkedAccounts = [ - { - provider: 'github', - data: { - handle: "github-handle", - followers: 1, - publicRepos: 1 - } - } - ]; + var linkedAccounts = angular.copy(mockLinkedAccounts); var template, element, controller; beforeEach(function() { @@ -48,6 +69,11 @@ describe('ExternalWebLinks Directive', function() { controller = element.controller('externalWebLink'); }); + afterEach(function() { + linkedAccounts = angular.copy(mockLinkedAccounts); + scope.linkedAccounts = linkedAccounts; + }); + it('should have added linkedAccounts to scope', function() { expect(scope.linkedAccounts).to.exist; expect(scope.linkedAccounts).to.have.length(1); @@ -56,12 +82,34 @@ describe('ExternalWebLinks Directive', function() { it('should have added new weblink to linkedAccounts', function() { scope.userHandle = 'test'; scope.url = 'https://www.topcoder.com'; - controller.addWebLink(); + element.isolateScope().addWebLink(); + scope.$digest(); + expect(scope.linkedAccounts).to.have.length(2); + var topcoderLink = _.find(scope.linkedAccounts, function(a) { + return a.provider === 'weblink' + }); + expect(topcoderLink).to.exist; + expect(topcoderLink.status).to.exist.to.equal('PENDING'); + expect(toasterSvc.pop).to.have.been.calledWith('success').calledOnce; + }); + + it('should NOT add new weblink to linkedAccounts', function() { + element.isolateScope().userHandle = 'throwError'; + element.isolateScope().url = 'https://www.topcoder.com'; + element.isolateScope().addWebLink(); + scope.$digest(); + expect(scope.linkedAccounts).to.have.length(1); + expect(toasterSvc.pop).to.have.been.calledWith('error').calledOnce; + }); + + it('should NOT add new weblink to linkedAccounts', function() { + element.isolateScope().userHandle = 'alreadyExistsError'; + element.isolateScope().url = 'https://www.topcoder.com'; + element.isolateScope().addWebLink(); scope.$digest(); - expect(extWebLinkSvc.addLink).to.have.been.calledOnce; - expect(controller.linkedAccounts).to.have.length(2); - expect(toasterSvc.pop).to.have.been.calledOnce; - }) + expect(scope.linkedAccounts).to.have.length(1); + expect(toasterSvc.pop).to.have.been.calledWith('error').calledOnce; + }); }); }); diff --git a/app/services/externalLinks.service.js b/app/services/externalLinks.service.js index 02327c3d6..8c8437401 100644 --- a/app/services/externalLinks.service.js +++ b/app/services/externalLinks.service.js @@ -3,9 +3,9 @@ angular.module('tc.services').factory('ExternalWebLinksService', ExternalWebLinksService); - ExternalWebLinksService.$inject = ['$log', 'CONSTANTS', 'ApiService']; + ExternalWebLinksService.$inject = ['$log', 'CONSTANTS', 'ApiService', '$q']; - function ExternalWebLinksService($log, CONSTANTS, ApiService) { + function ExternalWebLinksService($log, CONSTANTS, ApiService, $q) { $log = $log.getInstance("ExternalWebLinksService"); var memberApi = ApiService.getApiServiceProvider('MEMBER'); @@ -33,21 +33,37 @@ // add provider type as weblink links = _(links).forEach(function(l) { l.provider = 'weblink'; + if (l.synchronizedAt === 0) { + l.status = 'PENDING'; + } }).value(); return links; }); } function addLink(userHandle, url) { - return memberApi.one('members', userHandle).customPOST({'url': url}, 'externalLinks') + return $q(function(resolve, reject) { + memberApi.one('members', userHandle).customPOST({'url': url}, 'externalLinks') .then(function(resp) { var _newLink = { provider: 'weblink', data: resp }; - _newLink.data.status = 'pending'; - return _newLink; + _newLink.data.status = 'PENDING'; + resolve(_newLink); + }) + .catch(function(resp) { + var errorStatus = "FATAL_ERROR"; + $log.error("Error adding weblink: " + resp.data.result.content); + if (resp.data.result && resp.data.result.status === 400) { + errorStatus = "WEBLINK_ALREADY_EXISTS"; + } + reject({ + status: errorStatus, + msg: resp.data.result.content + }); }); + }); } function removeLink(userHandle, key) { diff --git a/app/services/externalLinks.service.spec.js b/app/services/externalLinks.service.spec.js index 0bbca515c..0b228456a 100644 --- a/app/services/externalLinks.service.spec.js +++ b/app/services/externalLinks.service.spec.js @@ -59,7 +59,21 @@ describe('ExternalWebLinks service', function() { expect(newLink).to.be.exist; expect(newLink.provider).to.exist.to.equal('weblink'); expect(newLink.data).to.exist; - expect(newLink.data.status).to.exist.to.equal('pending'); + expect(newLink.data.status).to.exist.to.equal('PENDING'); + }); + $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 + service.addLink('test1', "http://google.com").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_ALREADY_EXISTS'); + expect(error.msg).to.exist.to.equal(errorMessage); }); $httpBackend.flush(); }); diff --git a/app/settings/edit-profile/edit-profile.controller.js b/app/settings/edit-profile/edit-profile.controller.js index 6bdb5647e..dc182e84b 100644 --- a/app/settings/edit-profile/edit-profile.controller.js +++ b/app/settings/edit-profile/edit-profile.controller.js @@ -34,17 +34,6 @@ processData(vm.userData); - // ExternalAccountService.getLinkedExternalAccounts(vm.userData.userId).then(function(data) { - // vm.linkedExternalAccounts = data; - // }); - - // ExternalAccountService.getLinkedExternalLinksData(userHandle).then(function(data) { - // vm.linkedExternalAccountsData = data.plain(); - // vm.hasLinks = _.any(_.valuesIn(_.omit(vm.linkedExternalAccountsData, ['userId', 'updatedAt','createdAt','createdBy','updatedBy','handle']))); - // }) - // .catch(function(err) { - // $log.error(JSON.stringify(err)); - // }); var userId = vm.userData.userId; var userHandle = vm.userData.handle; var _linksPromises = [ diff --git a/app/settings/edit-profile/edit-profile.jade b/app/settings/edit-profile/edit-profile.jade index 5a4a3cce0..4b977b46f 100644 --- a/app/settings/edit-profile/edit-profile.jade +++ b/app/settings/edit-profile/edit-profile.jade @@ -98,7 +98,7 @@ .field-label Add a web link .web-links - external-web-link(linked-accounts="vm.linkedExternalAccounts", user-handle="{{vm.userData.handle}}") + external-web-link(linked-accounts="vm.linkedExternalAccountsData", user-handle="{{vm.userData.handle}}") .field-label Link Your Accounts diff --git a/assets/css/directives/external-link-data.scss b/assets/css/directives/external-link-data.scss index 76a3f1eb6..e71c29e20 100644 --- a/assets/css/directives/external-link-data.scss +++ b/assets/css/directives/external-link-data.scss @@ -67,7 +67,7 @@ external-accounts { } .bottom { width: 220px; - margin-top: 15px; + margin-top: 10px; .handle { @include sofia-pro-light; font-size: 18px; @@ -75,7 +75,7 @@ external-accounts { } .title { // placeholder - margin-top: 20px; + margin-top: 10px; color: #9e9e9e; font-size: 12px; line-height: 14px; @@ -122,13 +122,12 @@ external-accounts { @include sofia-pro-medium; font-size: 12px; line-height: 20px; - // to handle the missing title scenario - min-height: 20px; color: $gray-darkest; - margin-top: 20px; - max-height: 60px; + margin-top: 10px; + height: 40px; overflow: hidden; padding: 0px 20px; + text-transform: uppercase; } .link-url { @@ -137,8 +136,9 @@ external-accounts { word-wrap: break-word; display: block; overflow: hidden; - max-height: 28px; + max-height: 14px; padding: 0px 20px; + text-transform: uppercase; } } @@ -198,6 +198,7 @@ external-accounts { } .bottom { + margin-top: 15px; width: auto; .handle { margin-top: 20px; @@ -206,6 +207,8 @@ external-accounts { .title { // placeholder height: 40px; + margin-top: 15px; + margin-bottom: 30px; } ul { @@ -239,8 +242,13 @@ external-accounts { } .link-title { - margin-top: 20px; + margin-top: 15px; margin-bottom: 20px; + height: 60px; + } + + .link-url { + max-height: 28px; } } }