diff --git a/app/coffee/modules/resources/users.coffee b/app/coffee/modules/resources/users.coffee
index 3db62b96..cd7fca23 100644
--- a/app/coffee/modules/resources/users.coffee
+++ b/app/coffee/modules/resources/users.coffee
@@ -28,7 +28,7 @@ resourceProvider = ($http, $urls) ->
service = {}
service.contacts = (userId, options={}) ->
- url = $urls.resolve("contacts", userId)
+ url = $urls.resolve("user-contacts", userId)
httpOptions = {headers: {}}
if not options.enablePagination
diff --git a/app/modules/profile/includes/profile-favorites.jade b/app/modules/profile/includes/profile-favorites.jade
deleted file mode 100644
index f0a723c7..00000000
--- a/app/modules/profile/includes/profile-favorites.jade
+++ /dev/null
@@ -1,6 +0,0 @@
-section.profile-favorites
- nav.profile-favorites-filters
- a.active(href="", title="No Filter") all
- a(href="", title="Only show your team") projects
- a(href="", title="Only show people you follow") US
- a(href="", title="Only show people follow you") tasks
diff --git a/app/modules/profile/profile-contacts/profile-contacts.jade b/app/modules/profile/profile-contacts/profile-contacts.jade
index 265c26d7..06dfb26e 100644
--- a/app/modules/profile/profile-contacts/profile-contacts.jade
+++ b/app/modules/profile/profile-contacts/profile-contacts.jade
@@ -11,32 +11,30 @@ section.profile-contacts
div(ng-if="vm.isCurrentUser")
p(translate="USER.PROFILE.CURRENT_USER_CONTACTS_EMPTY")
p(translate="USER.PROFILE.CURRENT_USER_CONTACTS_EMPTY_EXPLAIN")
+ //-
+ nav.profile-contact-filters
+ a.active(href="", title="No Filter") all
+ a(href="", title="Only show your team") team
+ a(href="", title="Only show people you follow") following
+ a(href="", title="Only show people follow you") followers
- // nav.profile-contact-filters
- // a.active(href="", title="No Filter") all
- // a(href="", title="Only show your team") team
- // a(href="", title="Only show people you follow") following
- // a(href="", title="Only show people follow you") followers
+ div.list-itemtype-user(tg-repeat="contact in ::vm.contacts")
+ a.list-itemtype-avatar(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('name')}}")
+ img(ng-src="{{::contact.get('photo')}}", alt="{{::contact.get('full_name')}}")
- div.profile-contact-single(tg-repeat="contact in ::vm.contacts")
- div.profile-contact-picture
- a(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('name') }}")
- img(ng-src="{{::contact.get('photo')}}", alt="{{::contact.get('full_name')}}")
+ div.list-itemtype-user-data
+ h2
+ a(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('full_name_display') }}") {{::contact.get('full_name_display')}}
+
+ p {{::contact.get('roles').join(", ")}}
+ p.extra-info(ng-if="contact.get('bio')") {{::contact.get('bio')}}
- div.profile-contact-data
- h1
- a(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('full_name_display') }}")
- | {{::contact.get('full_name_display')}}
-
- p(ng-if="contact.bio") {{::contact.get('bio')}}
-
- div.extra-info
- span.position {{::contact.get('roles').join(", ")}}
- // span.location todo
- // div.profile-project-stats
- // div.stat-projects(title="2 projects")
- // span.icon.icon-project
- // span.stat-num 2
- // div.stat-viewer(title="2 followers")
- // span.icon.icon-open-eye
- // span.stat-num 4
+ //-
+ span.location todo
+ div.profile-project-stats
+ div.stat-projects(title="2 projects")
+ span.icon.icon-project
+ span.stat-num 2
+ div.stat-viewer(title="2 followers")
+ span.icon.icon-open-eye
+ span.stat-num 4
diff --git a/app/modules/profile/profile-favs/items/items.directive.coffee b/app/modules/profile/profile-favs/items/items.directive.coffee
new file mode 100644
index 00000000..5d7418ae
--- /dev/null
+++ b/app/modules/profile/profile-favs/items/items.directive.coffee
@@ -0,0 +1,20 @@
+FavItemDirective = ->
+ link = (scope, el, attrs, ctrl) ->
+ scope.vm = {item: scope.item}
+
+ templateUrl = (el, attrs) ->
+ if attrs.itemType == "project"
+ return "profile/profile-favs/items/project.html"
+ else # if attr.itemType in ["userstory", "task", "issue"]
+ return "profile/profile-favs/items/ticket.html"
+
+ return {
+ scope: {
+ "item": "=tgFavItem"
+ }
+ link: link
+ templateUrl: templateUrl
+ }
+
+
+angular.module("taigaProfile").directive("tgFavItem", FavItemDirective)
diff --git a/app/modules/profile/profile-favs/items/project.jade b/app/modules/profile/profile-favs/items/project.jade
new file mode 100644
index 00000000..2cb121d9
--- /dev/null
+++ b/app/modules/profile/profile-favs/items/project.jade
@@ -0,0 +1,34 @@
+.list-itemtype-project
+ .list-itemtype-project-data
+ h2
+ a(
+ href="#"
+ tg-nav="project:project=vm.item.get('slug')"
+ title="{{ ::vm.item.get('name') }}"
+ ) {{ ::vm.item.get('name') }}
+ span.private(ng-if="::project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
+ p {{ ::vm.item.get('description') }}
+
+ .list-itemtype-project-tags.tags-container(ng-if="::vm.item.get('tags_colors').size")
+ span.tag(
+ tg-repeat="tag in ::vm.item.get('tags_colors')"
+ style='border-left: 5px solid {{ ::tag.get("color") }};'
+ )
+ span.tag-name {{ ::tag.get('name') }}
+
+ .list-itemtype-track
+ span.list-itemtype-track-likers(
+ ng-class="{'active': vm.item.get('is_fan')}"
+ title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_fans\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../../svg/like.svg
+ span {{ ::vm.item.get('total_fans') }}
+
+ span.list-itemtype-track-watchers(
+ ng-class="{'active': vm.item.get('is_watcher')}"
+ title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_watchers\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../../svg/watch.svg
+ span {{ ::vm.item.get('total_watchers') }}
diff --git a/app/modules/profile/profile-favs/items/ticket.jade b/app/modules/profile/profile-favs/items/ticket.jade
new file mode 100644
index 00000000..f8051bc4
--- /dev/null
+++ b/app/modules/profile/profile-favs/items/ticket.jade
@@ -0,0 +1,80 @@
+div.list-itemtype-ticket
+ a.list-itemtype-avatar(
+ href=""
+ ng-if="::vm.item.get('assigned_to')"
+ tg-nav="user-profile:username=vm.item.get('assigned_to_username')"
+ title="{{ ::vm.item.get('assigned_to_full_name') }}"
+ )
+ img(
+ ng-src="{{ ::vm.item.get('assigned_to_photo') }}",
+ alt="{{ ::vm.item.get('assigned_to_full_name') }}"
+ )
+
+ a.list-itemtype-avatar(
+ href=""
+ ng-if="::!vm.item.get('assigned_to')",
+ title="{{ 'COMMON.ASSIGNED_TO.NOT_ASSIGNED'|translate }}"
+ )
+ img(
+ src="/images/unnamed.png",
+ alt="{{ 'COMMON.ASSIGNED_TO.NOT_ASSIGNED'|translate }}"
+ )
+
+ div.list-itemtype-ticket-data
+ p
+ span.ticket-project
+ | {{:: vm.item.get('project_name') }}
+ span.ticket-type(
+ ng-if="::vm.item.get('type') === 'userstory'"
+ translate="COMMON.USER_STORY"
+ )
+ span.ticket-type(
+ ng-if="::vm.item.get('type') === 'task'"
+ translate="COMMON.TASK"
+ )
+ span.ticket-type(
+ ng-if="::vm.item.get('type') === 'issue'"
+ translate="COMMON.ISSUE"
+ )
+ span.ticket-status(ng-style="::{'color': vm.item.get('status_color')}")
+ | {{:: vm.item.get('status') }}
+ h2
+ span.ticket-id(tg-bo-ref="vm.item.get('ref')")
+ a.ticket-title(
+ href="#"
+ ng-if="::vm.item.get('type') === 'userstory'"
+ tg-nav="project-userstories-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')"
+ title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}"
+ )
+ | {{ ::vm.item.get('subject') }}
+ a.ticket-title(
+ href="#"
+ ng-if="::vm.item.get('type') === 'task'"
+ tg-nav="project-tasks-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')"
+ title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}"
+ )
+ | {{ ::vm.item.get('subject') }}
+ a.ticket-title(
+ href="#"
+ ng-if="::vm.item.get('type') === 'issue'"
+ tg-nav="project-issues-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')"
+ title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}"
+ )
+ | {{ ::vm.item.get('subject') }}
+
+ div.list-itemtype-track
+ span.list-itemtype-track-likers(
+ ng-class="{'active': vm.item.get('is_voter')}",
+ title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_voters\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../../svg/upvote.svg
+ span {{ ::vm.item.get('total_voters') }}
+
+ span.list-itemtype-track-watchers(
+ ng-class="{'active': vm.item.get('is_watcher')}"
+ title="{{ 'COMMON.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_watchers\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../../svg/watch.svg
+ span {{ ::vm.item.get('total_watchers') }}
diff --git a/app/modules/profile/profile-favs/profile-favs.controller.coffee b/app/modules/profile/profile-favs/profile-favs.controller.coffee
new file mode 100644
index 00000000..b4ba78e0
--- /dev/null
+++ b/app/modules/profile/profile-favs/profile-favs.controller.coffee
@@ -0,0 +1,168 @@
+debounceLeading = @.taiga.debounceLeading
+
+class FavsBaseController
+ constructor: ->
+ @._init()
+
+ #@._getItems = null # Define in inheritance classes
+ #
+ _init: ->
+ @.enableFilterByAll = true
+ @.enableFilterByProjects = true
+ @.enableFilterByUserStories = true
+ @.enableFilterByTasks = true
+ @.enableFilterByIssues = true
+ @.enableFilterByTextQuery = true
+
+ @._resetList()
+ @.q = null
+ @.type = null
+
+ _resetList: ->
+ @.items = Immutable.List()
+ @.scrollDisabled = false
+ @._page = 1
+
+ _enableLoadingSpinner: ->
+ @.isLoading = true
+
+ _disableLoadingSpinner: ->
+ @.isLoading = false
+
+ _enableScroll : ->
+ @.scrollDisabled = false
+
+ _disableScroll : ->
+ @.scrollDisabled = true
+
+ _checkIfHasMorePages: (hasNext) ->
+ if hasNext
+ @._page += 1
+ @._enableScroll()
+ else
+ @._disableScroll()
+
+ _checkIfHasNoResults: ->
+ @.hasNoResults = @.items.size == 0
+
+ loadItems: ->
+ @._enableLoadingSpinner()
+ @._disableScroll()
+
+ @._getItems(@.user.get("id"), @._page, @.type, @.q)
+ .then (response) =>
+ @.items = @.items.concat(response.get("data"))
+
+ @._checkIfHasMorePages(response.get("next"))
+ @._checkIfHasNoResults()
+ @._disableLoadingSpinner()
+
+ return @.items
+ .catch =>
+ @._disableLoadingSpinner()
+
+ return @.items
+
+ ################################################
+ ## Filtre actions
+ ################################################
+ filterByTextQuery: debounceLeading 500, ->
+ @._resetList()
+ @.loadItems()
+
+ showAll: ->
+ if @.type isnt null
+ @.type = null
+ @._resetList()
+ @.loadItems()
+
+ showProjectsOnly: ->
+ if @.type isnt "project"
+ @.type = "project"
+ @._resetList()
+ @.loadItems()
+
+ showUserStoriesOnly: ->
+ if @.type isnt "userstory"
+ @.type = "userstory"
+ @._resetList()
+ @.loadItems()
+
+ showTasksOnly: ->
+ if @.type isnt "task"
+ @.type = "task"
+ @._resetList()
+ @.loadItems()
+
+ showIssuesOnly: ->
+ if @.type isnt "issue"
+ @.type = "issue"
+ @._resetList()
+ @.loadItems()
+
+
+####################################################
+## Liked
+####################################################
+
+class ProfileLikedController extends FavsBaseController
+ @.$inject = [
+ "tgUserService",
+ ]
+
+ constructor: (@userService) ->
+ super()
+ @.enableFilterByAll = false
+ @.enableFilterByProjects = false
+ @.enableFilterByUserStories = false
+ @.enableFilterByTasks = false
+ @.enableFilterByIssues = false
+ @.enableFilterByTextQuery = true
+ @._getItems = @userService.getLiked
+
+
+angular.module("taigaProfile")
+ .controller("ProfileLiked", ProfileLikedController)
+
+####################################################
+## Voted
+####################################################
+
+class ProfileVotedController extends FavsBaseController
+ @.$inject = [
+ "tgUserService",
+ ]
+
+ constructor: (@userService) ->
+ super()
+ @.enableFilterByAll = true
+ @.enableFilterByProjects = false
+ @.enableFilterByUserStories = true
+ @.enableFilterByTasks = true
+ @.enableFilterByIssues = true
+ @.enableFilterByTextQuery = true
+ @._getItems = @userService.getVoted
+
+
+angular.module("taigaProfile")
+ .controller("ProfileVoted", ProfileVotedController)
+
+
+
+####################################################
+## Watched
+####################################################
+
+class ProfileWatchedController extends FavsBaseController
+ @.$inject = [
+ "tgUserService",
+ ]
+
+ constructor: (@userService) ->
+ super()
+ @._getItems = @userService.getWatched
+
+
+angular.module("taigaProfile")
+ .controller("ProfileWatched", ProfileWatchedController)
+
diff --git a/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee b/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee
new file mode 100644
index 00000000..99e98b99
--- /dev/null
+++ b/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee
@@ -0,0 +1,679 @@
+describe "ProfileLiked", ->
+ $controller = null
+ provide = null
+ $rootScope = null
+ mocks = {}
+
+ user = Immutable.fromJS({id: 2})
+
+ _mockUserService = () ->
+ mocks.userServices = {
+ getLiked: sinon.stub()
+ }
+
+ provide.value "tgUserService", mocks.userServices
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockUserService()
+
+ return null
+
+ _inject = (callback) ->
+ inject (_$controller_, _$rootScope_) ->
+ $rootScope = _$rootScope_
+ $controller = _$controller_
+
+ beforeEach ->
+ module "taigaProfile"
+ _mocks()
+ _inject()
+
+ it "load paginated items", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileLiked", $scope, {user: user})
+
+ items1 = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+ items2 = Immutable.fromJS({
+ data: [
+ {id: 4},
+ {id: 5},
+ ],
+ next: false
+ })
+
+ mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, null).promise().resolve(items1)
+ mocks.userServices.getLiked.withArgs(user.get("id"), 2, null, null).promise().resolve(items2)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.loadItems().then () =>
+ expectItems = items1.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.loadItems().then () =>
+ expectItems = expectItems.concat(items2.get("data"))
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.true
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "filter items by text query", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileLiked", $scope, {user: user})
+
+ textQuery = "_test_"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, textQuery).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.q = textQuery
+
+ ctrl.loadItems().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.equal(textQuery)
+ done()
+
+ it "shou loading spinner during the call to the api", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileLiked", $scope, {user: user})
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mockPromise = mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, null).promise()
+
+ expect(ctrl.isLoading).to.be.undefined
+
+ promise = ctrl.loadItems()
+
+ expect(ctrl.isLoading).to.be.true
+
+ mockPromise.resolve(items)
+
+ promise.then () =>
+ expect(ctrl.isLoading).to.be.false
+ done()
+
+ it "shou no results placeholder", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileLiked", $scope, {user: user})
+
+ items = Immutable.fromJS({
+ data: [],
+ next: false
+ })
+
+ mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, null).promise().resolve(items)
+
+ expect(ctrl.hasNoResults).to.be.undefined
+
+ ctrl.loadItems().then () =>
+ expect(ctrl.hasNoResults).to.be.true
+ done()
+
+
+describe "ProfileVoted", ->
+ $controller = null
+ provide = null
+ $rootScope = null
+ mocks = {}
+
+ user = Immutable.fromJS({id: 2})
+
+ _mockUserService = () ->
+ mocks.userServices = {
+ getVoted: sinon.stub()
+ }
+
+ provide.value "tgUserService", mocks.userServices
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockUserService()
+
+ return null
+
+ _inject = (callback) ->
+ inject (_$controller_, _$rootScope_) ->
+ $rootScope = _$rootScope_
+ $controller = _$controller_
+
+ beforeEach ->
+ module "taigaProfile"
+ _mocks()
+ _inject()
+
+ it "load paginated items", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileVoted", $scope, {user: user})
+
+ items1 = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+ items2 = Immutable.fromJS({
+ data: [
+ {id: 4},
+ {id: 5},
+ ],
+ next: false
+ })
+
+ mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, null).promise().resolve(items1)
+ mocks.userServices.getVoted.withArgs(user.get("id"), 2, null, null).promise().resolve(items2)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.loadItems().then () =>
+ expectItems = items1.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.loadItems().then () =>
+ expectItems = expectItems.concat(items2.get("data"))
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.true
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "filter items by text query", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileVoted", $scope, {user: user})
+
+ textQuery = "_test_"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, textQuery).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.q = textQuery
+
+ ctrl.loadItems().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.equal(textQuery)
+ done()
+
+ it "show only items of user stories", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileVoted", $scope, {user: user})
+
+ type = "userstory"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getVoted.withArgs(user.get("id"), 1, type, null).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.showUserStoriesOnly().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.type
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "show only items of tasks", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileVoted", $scope, {user: user})
+
+ type = "task"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getVoted.withArgs(user.get("id"), 1, type, null).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.showTasksOnly().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.type
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "show only items of issues", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileVoted", $scope, {user: user})
+
+ type = "issue"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getVoted.withArgs(user.get("id"), 1, type, null).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.showIssuesOnly().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.type
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "shou loading spinner during the call to the api", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileVoted", $scope, {user: user})
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mockPromise = mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, null).promise()
+
+ expect(ctrl.isLoading).to.be.undefined
+
+ promise = ctrl.loadItems()
+
+ expect(ctrl.isLoading).to.be.true
+
+ mockPromise.resolve(items)
+
+ promise.then () =>
+ expect(ctrl.isLoading).to.be.false
+ done()
+
+ it "shou no results placeholder", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileVoted", $scope, {user: user})
+
+ items = Immutable.fromJS({
+ data: [],
+ next: false
+ })
+
+ mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, null).promise().resolve(items)
+
+ expect(ctrl.hasNoResults).to.be.undefined
+
+ ctrl.loadItems().then () =>
+ expect(ctrl.hasNoResults).to.be.true
+ done()
+
+describe "ProfileWatched", ->
+ $controller = null
+ provide = null
+ $rootScope = null
+ mocks = {}
+
+ user = Immutable.fromJS({id: 2})
+
+ _mockUserService = () ->
+ mocks.userServices = {
+ getWatched: sinon.stub()
+ }
+
+ provide.value "tgUserService", mocks.userServices
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockUserService()
+
+ return null
+
+ _inject = (callback) ->
+ inject (_$controller_, _$rootScope_) ->
+ $rootScope = _$rootScope_
+ $controller = _$controller_
+
+ beforeEach ->
+ module "taigaProfile"
+ _mocks()
+ _inject()
+
+ it "load paginated items", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ items1 = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+ items2 = Immutable.fromJS({
+ data: [
+ {id: 4},
+ {id: 5},
+ ],
+ next: false
+ })
+
+ mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, null).promise().resolve(items1)
+ mocks.userServices.getWatched.withArgs(user.get("id"), 2, null, null).promise().resolve(items2)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.loadItems().then () =>
+ expectItems = items1.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.loadItems().then () =>
+ expectItems = expectItems.concat(items2.get("data"))
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.true
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "filter items by text query", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ textQuery = "_test_"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, textQuery).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.q = textQuery
+
+ ctrl.loadItems().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.equal(textQuery)
+ done()
+
+ it "show only items of projects", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ type = "project"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.showProjectsOnly().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.type
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "show only items of user stories", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ type = "userstory"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.showUserStoriesOnly().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.type
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "show only items of tasks", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ type = "task"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.showTasksOnly().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.type
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "show only items of issues", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ type = "issue"
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items)
+
+ expect(ctrl.items.size).to.be.equal(0)
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.null
+ expect(ctrl.q).to.be.null
+
+ ctrl.showIssuesOnly().then () =>
+ expectItems = items.get("data")
+
+ expect(ctrl.items.equals(expectItems)).to.be.true
+ expect(ctrl.scrollDisabled).to.be.false
+ expect(ctrl.type).to.be.type
+ expect(ctrl.q).to.be.null
+ done()
+
+ it "shou loading spinner during the call to the api", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ items = Immutable.fromJS({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ],
+ next: true
+ })
+
+ mockPromise = mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, null).promise()
+
+ expect(ctrl.isLoading).to.be.undefined
+
+ promise = ctrl.loadItems()
+
+ expect(ctrl.isLoading).to.be.true
+
+ mockPromise.resolve(items)
+
+ promise.then () =>
+ expect(ctrl.isLoading).to.be.false
+ done()
+
+ it "shou no results placeholder", (done) ->
+ $scope = $rootScope.$new()
+ ctrl = $controller("ProfileWatched", $scope, {user: user})
+
+ items = Immutable.fromJS({
+ data: [],
+ next: false
+ })
+
+ mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, null).promise().resolve(items)
+
+ expect(ctrl.hasNoResults).to.be.undefined
+
+ ctrl.loadItems().then () =>
+ expect(ctrl.hasNoResults).to.be.true
+ done()
diff --git a/app/modules/profile/profile-favs/profile-favs.directive.coffee b/app/modules/profile/profile-favs/profile-favs.directive.coffee
new file mode 100644
index 00000000..91bfe690
--- /dev/null
+++ b/app/modules/profile/profile-favs/profile-favs.directive.coffee
@@ -0,0 +1,50 @@
+base = {
+ scope: {},
+ bindToController: {
+ user: "="
+ type: "@"
+ q: "@"
+ scrollDisabled: "@"
+ isLoading: "@"
+ hasNoResults: "@"
+ }
+ controller: null, # Define in directives
+ controllerAs: "vm",
+ templateUrl: "profile/profile-favs/profile-favs.html",
+}
+
+
+####################################################
+## Liked
+####################################################
+
+ProfileLikedDirective = () ->
+ return _.extend({}, base, {
+ controller: "ProfileLiked"
+ })
+
+angular.module("taigaProfile").directive("tgProfileLiked", ProfileLikedDirective)
+
+
+####################################################
+## Voted
+####################################################
+
+ProfileVotedDirective = () ->
+ return _.extend({}, base, {
+ controller: "ProfileVoted"
+ })
+
+angular.module("taigaProfile").directive("tgProfileVoted", ProfileVotedDirective)
+
+
+####################################################
+## Watched
+####################################################
+
+ProfileWatchedDirective = () ->
+ return _.extend({}, base, {
+ controller: "ProfileWatched"
+ })
+
+angular.module("taigaProfile").directive("tgProfileWatched", ProfileWatchedDirective)
diff --git a/app/modules/profile/profile-favs/profile-favs.jade b/app/modules/profile/profile-favs/profile-favs.jade
new file mode 100644
index 00000000..38d976e3
--- /dev/null
+++ b/app/modules/profile/profile-favs/profile-favs.jade
@@ -0,0 +1,80 @@
+section.profile-favs
+ div.profile-filter
+ div.searchbox(ng-if="::vm.enableFilterByTextQuery")
+ span.icon-search
+ input(
+ type="text"
+ ng-model="vm.q"
+ ng-change="vm.filterByTextQuery()"
+ placeholder="{{ 'USER.PROFILE_FAVS.FILTER_INPUT_PLACEHOLDER'|translate }}"
+ )
+
+ div.filters
+ a(
+ href=""
+ ng-if="::vm.enableFilterByAll"
+ ng-click="vm.showAll()"
+ ng-class="{active: vm.type === null}"
+ title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ALL_TITLE'|translate }}"
+ translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ALL'|translate }}"
+ )
+ a(
+ href=""
+ ng-if="::vm.enableFilterByProjects"
+ ng-click="vm.showProjectsOnly()"
+ ng-class="{active: vm.type === 'project'}"
+ title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_PROJECTS_TITLE'|translate }}"
+ translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_PROJECTS'|translate }}"
+ )
+ a(
+ href=""
+ ng-if="::vm.enableFilterByUserStories"
+ ng-click="vm.showUserStoriesOnly()"
+ ng-class="{active: vm.type === 'userstory'}",
+ title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_USER_STORIES_TITLE'|translate }}"
+ translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_USER_STORIES'|translate }}"
+ )
+ a(
+ href=""
+ ng-if="::vm.enableFilterByTasks"
+ ng-click="vm.showTasksOnly()"
+ ng-class="{active: vm.type === 'task'}"
+ title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_TASKS_TITLE'|translate }}"
+ translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_TASKS'|translate }}"
+ )
+ a(
+ href=""
+ ng-if="::vm.enableFilterByIssues"
+ ng-click="vm.showIssuesOnly()"
+ ng-class="{active: vm.type === 'issue'}"
+ title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ISSUES_TITLE'|translate }}"
+ translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ISSUES'|translate }}"
+ )
+
+ div(
+ infinite-scroll="vm.loadItems()"
+ infinite-scroll-distance="2"
+ infinite-scroll-disabled="vm.scrollDisabled"
+ )
+ div(
+ tg-repeat="item in vm.items track by $index"
+ ng-switch="item.get('type')"
+ )
+ div(ng-switch-when="project", tg-fav-item="item", item-type="project")
+ div(ng-switch-when="userstory", tg-fav-item="item", item-type="userstory")
+ div(ng-switch-when="task", tg-fav-item="item", item-type="task")
+ div(ng-switch-when="issue", tg-fav-item="item", item-type="issue")
+
+ div(ng-if="vm.isLoading")
+ div.spin
+ img(
+ src="/svg/spinner-circle.svg"
+ alt="{{ 'COMMON.LOADING'|translate }}"
+ )
+
+ .empty-search-results(ng-if="vm.hasNoResults && !vm.isLoading")
+ img(
+ src="../../images/search-empty.png"
+ alt="{{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }}"
+ )
+ p.title {{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }}
diff --git a/app/modules/profile/profile-favs/profile-favs.scss b/app/modules/profile/profile-favs/profile-favs.scss
new file mode 100644
index 00000000..e37d4542
--- /dev/null
+++ b/app/modules/profile/profile-favs/profile-favs.scss
@@ -0,0 +1,43 @@
+.profile-favs {
+ border-top: 1px solid $whitish;
+}
+
+.profile-filter {
+ align-items: center;
+ background: $whitish;
+ display: flex;
+ justify-content: space-between;
+ margin: 1rem 0;
+ padding: .5rem 1rem;
+ .searchbox {
+ align-items: center;
+ display: flex;
+ flex: 1;
+ .icon-search {
+ color: grayer;
+ margin-right: .5rem;
+ }
+ input {
+ border: 0;
+ border-bottom: 1px solid transparent;
+ flex: 1;
+ margin-right: 1rem;
+ &:focus {
+ border-bottom: 1px solid $gray-light;
+ outline: none;
+ transition: border-bottom .3s ease-in;
+ }
+ }
+ }
+ .filters {
+ a {
+ color: $gray-light;
+ display: inline-block;
+ padding: 0 .5rem;
+ &:hover,
+ &.active {
+ color: $blackish;
+ }
+ }
+ }
+}
diff --git a/app/modules/profile/profile-projects/profile-projects.jade b/app/modules/profile/profile-projects/profile-projects.jade
index 4b5a8bd5..c9d10a0f 100644
--- a/app/modules/profile/profile-projects/profile-projects.jade
+++ b/app/modules/profile/profile-projects/profile-projects.jade
@@ -3,37 +3,56 @@ section.profile-projects
div.spin
img(src="/svg/spinner-circle.svg", alt="Loading...")
- div.empty-tab(ng-if="vm.projects && !vm.projects.size")
+ .empty-tab(ng-if="vm.projects && !vm.projects.size")
include ../../../svg/hide.svg
- p(translate="USER.PROFILE.PROJECTS_EMPTY", translate-values="{username: vm.user.get('full_name_display')}")
+ p(
+ translate="USER.PROFILE.PROJECTS_EMPTY"
+ translate-values="{username: vm.user.get('full_name_display')}"
+ )
- div.project-list-single(tg-repeat="project in vm.projects")
- div.project-list-single-left
+ .list-itemtype-project(tg-repeat="project in vm.projects")
+ .list-itemtype-project-left
- div.project-list-single-title
- h1
- a(href="#", tg-nav="project:project=project.get('slug')", title="{{ ::project.get('name') }}") {{::project.get('name')}}
+ .project-list-single-title
+ h2
+ a(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{ ::project.get('name') }}"
+ ) {{::project.get('name')}}
p {{ ::project.get('description') | limitTo:300 }}
- div.project-list-single-tags.tags-container(ng-if="::project.get('tags').size")
- span.tag(style='border-left: 5px solid {{::tag.get("color")}};', tg-repeat="tag in ::project.get('colorized_tags')")
+ .list-itemtype-project-tags.tags-container(ng-if="::project.get('tags').size")
+ span.tag(
+ style='border-left: 5px solid {{::tag.get("color")}};'
+ tg-repeat="tag in ::project.get('colorized_tags')"
+ )
span.tag-name {{::tag.get('name')}}
- div.project-list-single-right
+ .list-itemtype-project-right
- div.project-list-single-members
- a(tg-repeat="contact in ::project.get('contacts')", tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('full_name')}}")
+ .list-itemtype-track
+ span.list-itemtype-track-likers(
+ ng-class="{'active': project.get('is_fan')}"
+ title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:project.get(\"total_fans\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../svg/like.svg
+ span {{ ::project.get('total_fans') }}
+
+ span.list-itemtype-track-watchers(
+ ng-class="{'active': project.get('is_watcher')}"
+ title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:project.get(\"total_watchers\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../svg/watch.svg
+ span {{ ::project.get('total_watchers') }}
+
+ .list-itemtype-project-members
+ a(
+ tg-repeat="contact in ::project.get('contacts')"
+ tg-nav="user-profile:username=contact.get('username')"
+ title="{{::contact.get('full_name')}}"
+ )
img(ng-src="{{::contact.get('photo')}}")
-
- // div.project-list-single-right
- // div.project-list-single-stats
- // div.stat-comments(title="2 comments")
- // span.icon.icon-comment
- // span.stat-num 2
- // div.stat-favorite.active(title="2 favorites")
- // span.icon.icon-star-fill
- // span.stat-num 4
- // div.stat-viewer(title="2 followers")
- // span.icon.icon-open-eye
- // span.stat-num 4
diff --git a/app/modules/profile/profile-tab/profile-tab.directive.coffee b/app/modules/profile/profile-tab/profile-tab.directive.coffee
index 8312ddd4..3e4983da 100644
--- a/app/modules/profile/profile-tab/profile-tab.directive.coffee
+++ b/app/modules/profile/profile-tab/profile-tab.directive.coffee
@@ -2,10 +2,12 @@ ProfileTabDirective = () ->
link = (scope, element, attrs, ctrl, transclude) ->
scope.tab = {}
+ attrs.$observe "tgProfileTab", (name) ->
+ scope.tab.name = name
+
attrs.$observe "tabTitle", (title) ->
scope.tab.title = title
- scope.tab.name = attrs.tgProfileTab
scope.tab.icon = attrs.tabIcon
scope.tab.active = !!attrs.tabActive
diff --git a/app/modules/profile/profile-tabs/profile-tabs.jade b/app/modules/profile/profile-tabs/profile-tabs.jade
index 00390cd8..ba10af3a 100644
--- a/app/modules/profile/profile-tabs/profile-tabs.jade
+++ b/app/modules/profile/profile-tabs/profile-tabs.jade
@@ -1,7 +1,13 @@
div
nav.profile-content-tabs
- a.tab(ng-repeat="tab in ::vm.tabs", href="", title="{{tab.title}}", ng-class="{active: tab.active}" ng-click="vm.toggleTab(tab)")
+ a.tab(
+ href=""
+ ng-repeat="tab in ::vm.tabs"
+ title="{{tab.title}}"
+ ng-click="vm.toggleTab(tab)"
+ ng-class="{active: tab.active}"
+ )
span.icon(ng-class="::tab.icon")
span {{::tab.name}}
- ng-transclude
\ No newline at end of file
+ ng-transclude
diff --git a/app/modules/profile/profile.jade b/app/modules/profile/profile.jade
index 18058610..dc5ce3f7 100644
--- a/app/modules/profile/profile.jade
+++ b/app/modules/profile/profile.jade
@@ -2,16 +2,48 @@ div.profile.centered(ng-if="vm.user")
div(tg-profile-bar, user="vm.user", isCurrentUser="vm.isCurrentUser")
div.main
div.timeline-wrapper(tg-profile-tabs)
- div(tg-profile-tab="activity", tab-title="{{'USER.PROFILE.ACTIVITY_TAB' | translate}}", tab-icon="icon-timeline", tab-active)
- div(tg-user-timeline, user="vm.user", current-user="vm.isCurrentUser")
+ div(
+ tg-profile-tab="{{'USER.PROFILE.TABS.ACTIVITY_TAB' | translate}}"
+ tab-title="{{'USER.PROFILE.TABS.ACTIVITY_TAB_TITLE' | translate}}"
+ tab-icon="icon-timeline"
+ tab-active
+ )
+ div(tg-user-timeline, user="vm.user", current-user="vm.isCurrentUser")
- div(tab-disabled="{{vm.isCurrentUser}}", tg-profile-tab="projects", tab-title="{{'USER.PROFILE.PROJECTS_TAB' | translate}}", tab-icon="icon-project")
- div(tg-profile-projects, user="vm.user")
+ div(
+ tg-profile-tab="{{'USER.PROFILE.TABS.PROJECTS_TAB' | translate}}"
+ tab-title="{{'USER.PROFILE.TABS.PROJECTS_TAB_TITLE' | translate}}"
+ tab-icon="icon-project"
+ tab-disabled="{{vm.isCurrentUser}}"
+ )
+ div(tg-profile-projects, user="vm.user")
- div(tg-profile-tab="contacts", tab-title="{{'USER.PROFILE.CONTACTS_TAB' | translate}}", tab-icon="icon-team")
- div(tg-profile-contacts, user="vm.user")
+ div(
+ tg-profile-tab="{{'USER.PROFILE.TABS.LIKES_TAB' | translate}}"
+ tab-title="{{'USER.PROFILE.TABS.LIKES_TAB_TITLE' | translate}}"
+ tab-icon="icon-heart"
+ )
+ div(tg-profile-liked, user="vm.user")
- // div(tg-profile-tab="favorites", tab-title="{{'USER.PROFILE.FAVORITES_TAB' | translate}}", tab-icon="icon-star-fill")
- // include includes/profile-favorites
+ div(
+ tg-profile-tab="{{'USER.PROFILE.TABS.VOTES_TAB' | translate}}"
+ tab-title="{{'USER.PROFILE.TABS.VOTES_TAB_TITLE' | translate}}"
+ tab-icon="icon-caret-up"
+ )
+ div(tg-profile-voted, user="vm.user")
+
+ div(
+ tg-profile-tab="{{'USER.PROFILE.TABS.WATCHED_TAB' | translate}}"
+ tab-title="{{'USER.PROFILE.TABS.WATCHED_TAB_TITLE' | translate}}"
+ tab-icon="icon-eye"
+ )
+ div(tg-profile-watched, user="vm.user")
+
+ div(
+ tg-profile-tab="{{'USER.PROFILE.TABS.CONTACTS_TAB' | translate}}"
+ tab-title="{{'USER.PROFILE.TABS.CONTACTS_TAB_TITLE' | translate}}"
+ tab-icon="icon-team"
+ )
+ div(tg-profile-contacts, user="vm.user")
include includes/profile-sidebar
diff --git a/app/modules/profile/styles/profile-favorites.scss b/app/modules/profile/styles/profile-favorites.scss
deleted file mode 100644
index 43212cd5..00000000
--- a/app/modules/profile/styles/profile-favorites.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.profile-favorites {
- border-top: 1px solid $whitish;
- display: flex;
- flex-direction: column;
- .profile-favorites-filters {
- align-self: flex-start;
- display: flex;
- a {
- border-bottom: 2px solid $white;
- color: $gray-light;
- display: inline-block;
- padding: 1rem 1.5rem;
- transition: all .2s linear;
- &:hover,
- &.active {
- border-bottom: 2px solid $gray-light;
- color: $primary;
- }
- }
- }
-}
diff --git a/app/modules/resources/projects-resource.service.coffee b/app/modules/resources/projects-resource.service.coffee
index 806bc550..6a674676 100644
--- a/app/modules/resources/projects-resource.service.coffee
+++ b/app/modules/resources/projects-resource.service.coffee
@@ -51,6 +51,25 @@ Resource = (urlsService, http, paginateResponseService) ->
result = Immutable.fromJS(result)
return paginateResponseService(result)
+ service.likeProject = (projectId) ->
+ url = urlsService.resolve("project-like", projectId)
+ return http.post(url)
+
+ service.unlikeProject = (projectId) ->
+ url = urlsService.resolve("project-unlike", projectId)
+ return http.post(url)
+
+ service.watchProject = (projectId, notifyPolicy) ->
+ data = {
+ notify_policy: notifyPolicy
+ }
+ url = urlsService.resolve("project-watch", projectId)
+ return http.post(url, data)
+
+ service.unwatchProject = (projectId) ->
+ url = urlsService.resolve("project-unwatch", projectId)
+ return http.post(url)
+
return () ->
return {"projects": service}
diff --git a/app/modules/resources/users-resource.service.coffee b/app/modules/resources/users-resource.service.coffee
index 57e0a8eb..ed89921e 100644
--- a/app/modules/resources/users-resource.service.coffee
+++ b/app/modules/resources/users-resource.service.coffee
@@ -19,7 +19,7 @@ Resource = (urlsService, http, paginateResponseService) ->
return Immutable.fromJS(result.data)
service.getStats = (userId) ->
- url = urlsService.resolve("stats", userId)
+ url = urlsService.resolve("user-stats", userId)
httpOptions = {
headers: {
@@ -32,7 +32,7 @@ Resource = (urlsService, http, paginateResponseService) ->
return Immutable.fromJS(result.data)
service.getContacts = (userId) ->
- url = urlsService.resolve("contacts", userId)
+ url = urlsService.resolve("user-contacts", userId)
httpOptions = {
headers: {
@@ -44,6 +44,45 @@ Resource = (urlsService, http, paginateResponseService) ->
.then (result) ->
return Immutable.fromJS(result.data)
+ service.getLiked = (userId, page, type, q) ->
+ url = urlsService.resolve("user-liked", userId)
+
+ params = {}
+ params.page = page if page?
+ params.type = type if type?
+ params.q = q if q?
+
+ return http.get(url, params)
+ .then (result) ->
+ result = Immutable.fromJS(result)
+ return paginateResponseService(result)
+
+ service.getVoted = (userId, page, type, q) ->
+ url = urlsService.resolve("user-voted", userId)
+
+ params = {}
+ params.page = page if page?
+ params.type = type if type?
+ params.q = q if q?
+
+ return http.get(url, params)
+ .then (result) ->
+ result = Immutable.fromJS(result)
+ return paginateResponseService(result)
+
+ service.getWatched = (userId, page, type, q) ->
+ url = urlsService.resolve("user-watched", userId)
+
+ params = {}
+ params.page = page if page?
+ params.type = type if type?
+ params.q = q if q?
+
+ return http.get(url, params)
+ .then (result) ->
+ result = Immutable.fromJS(result)
+ return paginateResponseService(result)
+
service.getProfileTimeline = (userId, page) ->
params = {
page: page
diff --git a/app/modules/services/app-meta.service.coffee b/app/modules/services/app-meta.service.coffee
index eee818b0..545c1099 100644
--- a/app/modules/services/app-meta.service.coffee
+++ b/app/modules/services/app-meta.service.coffee
@@ -3,7 +3,13 @@ taiga = @.taiga
truncate = taiga.truncate
-class AppMetaService extends taiga.Service = ->
+class AppMetaService
+ @.$inject = [
+ "$rootScope"
+ ]
+
+ constructor: (@rootScope) ->
+
_set: (key, value) ->
return if not key
@@ -60,12 +66,19 @@ class AppMetaService extends taiga.Service = ->
@.setOpenGraphMetas(title, description)
addMobileViewport: () ->
- $('head').append(
- ''
+ $("head").append(
+ ""
)
removeMobileViewport: () ->
- $('meta[name="viewport"]').remove()
+ $("meta[name=\"viewport\"]").remove()
+
+ setfn: (fn) ->
+ @._listener() if @.listener
+
+ @._listener = @rootScope.$watchCollection fn, (metas) =>
+ @.setAll(metas.title, metas.description)
angular.module("taigaCommon").service("tgAppMetaService", AppMetaService)
diff --git a/app/modules/services/app-meta.service.spec.coffee b/app/modules/services/app-meta.service.spec.coffee
index ede2294d..c5f6f3cb 100644
--- a/app/modules/services/app-meta.service.spec.coffee
+++ b/app/modules/services/app-meta.service.spec.coffee
@@ -1,13 +1,15 @@
describe "AppMetaService", ->
appMetaService = null
+ $rootScope = null
data = {
title: "--title--",
description: "--description--"
}
_inject = () ->
- inject (_tgAppMetaService_) ->
+ inject (_tgAppMetaService_, _$rootScope_) ->
appMetaService = _tgAppMetaService_
+ $rootScope = _$rootScope_
beforeEach ->
module "taigaCommon"
@@ -53,3 +55,18 @@ describe "AppMetaService", ->
expect($("meta[property='og:description']")).to.have.attr("content", data.description)
expect($("meta[property='og:image']")).to.have.attr("content", "#{window.location.origin}/images/logo-color.png")
expect($("meta[property='og:url']")).to.have.attr("content", window.location.href)
+
+ it "set function to set the metas", () ->
+ fn = () ->
+ return {
+ title: 'test',
+ description: 'test2'
+ }
+
+
+ appMetaService.setAll = sinon.stub()
+ appMetaService.setfn(fn)
+
+ $rootScope.$digest()
+
+ expect(appMetaService.setAll).to.have.been.calledWith('test', 'test2')
diff --git a/app/modules/services/current-user.service.coffee b/app/modules/services/current-user.service.coffee
index 21d7c217..69442831 100644
--- a/app/modules/services/current-user.service.coffee
+++ b/app/modules/services/current-user.service.coffee
@@ -49,13 +49,7 @@ class CurrentUserService
loadProjects: () ->
return @projectsService.getProjectsByUserId(@._user.get("id"))
- .then (projects) =>
- @._projects = @._projects.set("all", projects)
- @._projects = @._projects.set("recents", projects.slice(0, 10))
-
- @._projectsById = Immutable.fromJS(groupBy(projects.toJS(), (p) -> p.id))
-
- return @.projects
+ .then (projects) => @.setProjects(projects)
disableJoyRide: (section) ->
if section
@@ -96,4 +90,12 @@ class CurrentUserService
@.loadProjects()
])
+ setProjects: (projects) ->
+ @._projects = @._projects.set("all", projects)
+ @._projects = @._projects.set("recents", projects.slice(0, 10))
+
+ @._projectsById = Immutable.fromJS(groupBy(projects.toJS(), (p) -> p.id))
+
+ return @.projects
+
angular.module("taigaCommon").service("tgCurrentUserService", CurrentUserService)
diff --git a/app/modules/services/current-user.service.spec.coffee b/app/modules/services/current-user.service.spec.coffee
index 7a418ff0..f547558e 100644
--- a/app/modules/services/current-user.service.spec.coffee
+++ b/app/modules/services/current-user.service.spec.coffee
@@ -92,7 +92,7 @@ describe "tgCurrentUserService", ->
it "bulkUpdateProjectsOrder and reload projects", (done) ->
fakeData = [{id: 1, id: 2}]
- currentUserService.loadProjects = sinon.spy()
+ currentUserService.loadProjects = sinon.stub()
mocks.projectsService.bulkUpdateProjectsOrder.withArgs(fakeData).promise().resolve()
@@ -101,6 +101,41 @@ describe "tgCurrentUserService", ->
done()
+ it "loadProject and set it", (done) ->
+ user = Immutable.fromJS({id: 1, name: "fake1"})
+ project = Immutable.fromJS({id: 2, name: "fake2"})
+
+ currentUserService._user = user
+ currentUserService.setProjects = sinon.stub()
+
+ mocks.projectsService.getProjectsByUserId.withArgs(1).promise().resolve(project)
+
+ currentUserService.loadProjects().then () ->
+ expect(currentUserService.setProjects).to.have.been.calledWith(project)
+
+ done()
+
+ it "setProject", () ->
+ projectsRaw = [
+ {id: 1, name: "fake1"},
+ {id: 2, name: "fake2"},
+ {id: 3, name: "fake3"},
+ {id: 4, name: "fake4"}
+ ]
+ projectsRawById = {
+ 1: {id: 1, name: "fake1"},
+ 2: {id: 2, name: "fake2"},
+ 3: {id: 3, name: "fake3"},
+ 4: {id: 4, name: "fake4"}
+ }
+ projects = Immutable.fromJS(projectsRaw)
+
+ currentUserService.setProjects(projects)
+
+ expect(currentUserService.projects.get('all').toJS()).to.be.eql(projectsRaw)
+ expect(currentUserService.projects.get('recents').toJS()).to.be.eql(projectsRaw)
+ expect(currentUserService.projectsById.toJS()).to.be.eql(projectsRawById)
+
it "is authenticated", () ->
currentUserService.getUser = sinon.stub()
currentUserService.getUser.returns({})
diff --git a/app/modules/services/user.service.coffee b/app/modules/services/user.service.coffee
index f6c803d8..cc376569 100644
--- a/app/modules/services/user.service.coffee
+++ b/app/modules/services/user.service.coffee
@@ -1,9 +1,12 @@
taiga = @.taiga
+bindMethods = taiga.bindMethods
+
class UserService extends taiga.Service
@.$inject = ["tgResources"]
constructor: (@rs) ->
+ bindMethods(@)
getUserByUserName: (username) ->
return @rs.users.getUserByUsername(username)
@@ -11,6 +14,15 @@ class UserService extends taiga.Service
getContacts: (userId) ->
return @rs.users.getContacts(userId)
+ getLiked: (userId, pageNumber, objectType, textQuery) ->
+ return @rs.users.getLiked(userId, pageNumber, objectType, textQuery)
+
+ getVoted: (userId, pageNumber, objectType, textQuery) ->
+ return @rs.users.getVoted(userId, pageNumber, objectType, textQuery)
+
+ getWatched: (userId, pageNumber, objectType, textQuery) ->
+ return @rs.users.getWatched(userId, pageNumber, objectType, textQuery)
+
getStats: (userId) ->
return @rs.users.getStats(userId)
diff --git a/app/modules/services/user.service.spec.coffee b/app/modules/services/user.service.spec.coffee
index eb8156a7..9645350a 100644
--- a/app/modules/services/user.service.spec.coffee
+++ b/app/modules/services/user.service.spec.coffee
@@ -32,19 +32,6 @@ describe "UserService", ->
_mocks()
_inject()
- it "get user contacts", () ->
- userId = 2
-
- contacts = [
- {id: 1},
- {id: 2},
- {id: 3}
- ]
-
- mocks.resources.users.getContacts.withArgs(userId).returns(true)
-
- expect(userService.getContacts(userId)).to.be.true
-
it "attach user contacts to projects", (done) ->
userId = 2
@@ -88,6 +75,75 @@ describe "UserService", ->
$rootScope.$apply()
+ it "get user liked", (done) ->
+ userId = 2
+ pageNumber = 1
+ objectType = null
+ textQuery = null
+
+ liked = [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ]
+
+ mocks.resources.users.getLiked = sinon.stub()
+ mocks.resources.users.getLiked.withArgs(userId, pageNumber, objectType, textQuery)
+ .promise()
+ .resolve(liked)
+
+ userService.getLiked(userId, pageNumber, objectType, textQuery).then (_liked_) ->
+ expect(_liked_).to.be.eql(liked)
+ done()
+
+ $rootScope.$apply()
+
+ it "get user voted", (done) ->
+ userId = 2
+ pageNumber = 1
+ objectType = null
+ textQuery = null
+
+ voted = [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ]
+
+ mocks.resources.users.getVoted = sinon.stub()
+ mocks.resources.users.getVoted.withArgs(userId, pageNumber, objectType, textQuery)
+ .promise()
+ .resolve(voted)
+
+ userService.getVoted(userId, pageNumber, objectType, textQuery).then (_voted_) ->
+ expect(_voted_).to.be.eql(voted)
+ done()
+
+ $rootScope.$apply()
+
+ it "get user watched", (done) ->
+ userId = 2
+ pageNumber = 1
+ objectType = null
+ textQuery = null
+
+ watched = [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ]
+
+ mocks.resources.users.getWatched = sinon.stub()
+ mocks.resources.users.getWatched.withArgs(userId, pageNumber, objectType, textQuery)
+ .promise()
+ .resolve(watched)
+
+ userService.getWatched(userId, pageNumber, objectType, textQuery).then (_watched_) ->
+ expect(_watched_).to.be.eql(watched)
+ done()
+
+ $rootScope.$apply()
+
it "get user by username", (done) ->
username = "username-1"