Create like and watch buttons in project detail pages

stable
Juanfran 2015-10-23 16:46:46 +02:00 committed by David Barragán Merino
parent d1349c4272
commit f52935f8c9
21 changed files with 1013 additions and 205 deletions

View File

@ -0,0 +1,40 @@
class LikeProjectButtonController
@.$inject = [
"$tgConfirm"
"tgLikeProjectButtonService"
]
constructor: (@confirm, @likeButtonService)->
@.isMouseOver = false
@.loading = false
showTextWhenMouseIsOver: ->
@.isMouseOver = true
showTextWhenMouseIsLeave: ->
@.isMouseOver = false
toggleLike: ->
@.loading = true
if not @.project.get("is_fan")
promise = @._like()
else
promise = @._unlike()
promise.finally () => @.loading = false
return promise
_like: ->
return @likeButtonService.like(@.project.get('id'))
.then =>
@.showTextWhenMouseIsLeave()
.catch =>
@confirm.notify("error")
_unlike: ->
return @likeButtonService.unlike(@.project.get('id')).catch =>
@confirm.notify("error")
angular.module("taigaProjects").controller("LikeProjectButton", LikeProjectButtonController)

View File

@ -0,0 +1,115 @@
describe "LikeProjectButton", ->
$provide = null
$controller = null
mocks = {}
_mockTgConfirm = ->
mocks.tgConfirm = {
notify: sinon.stub()
}
$provide.value("$tgConfirm", mocks.tgConfirm)
_mockTgLikeProjectButton = ->
mocks.tgLikeProjectButton = {
like: sinon.stub(),
unlike: sinon.stub()
}
$provide.value("tgLikeProjectButtonService", mocks.tgLikeProjectButton)
_mocks = ->
module (_$provide_) ->
$provide = _$provide_
_mockTgConfirm()
_mockTgLikeProjectButton()
return null
_inject = ->
inject (_$controller_) ->
$controller = _$controller_
_setup = ->
_mocks()
_inject()
beforeEach ->
module "taigaProjects"
_setup()
it "toggleLike false -> true", (done) ->
project = Immutable.fromJS({
id: 3,
is_fan: false
})
ctrl = $controller("LikeProjectButton")
ctrl.project = project
mocks.tgLikeProjectButton.like = sinon.stub().promise()
promise = ctrl.toggleLike()
expect(ctrl.loading).to.be.true;
mocks.tgLikeProjectButton.like.withArgs(project.get('id')).resolve()
promise.finally () ->
expect(mocks.tgLikeProjectButton.like).to.be.calledOnce
expect(ctrl.loading).to.be.false;
done()
it "toggleLike false -> true, notify error", (done) ->
project = Immutable.fromJS({
id: 3,
is_fan: false
})
ctrl = $controller("LikeProjectButton")
ctrl.project = project
mocks.tgLikeProjectButton.like.withArgs(project.get('id')).promise().reject()
ctrl.toggleLike().finally () ->
expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce
done()
it "toggleLike true -> false", (done) ->
project = Immutable.fromJS({
is_fan: true
})
ctrl = $controller("LikeProjectButton")
ctrl.project = project
mocks.tgLikeProjectButton.unlike = sinon.stub().promise()
promise = ctrl.toggleLike()
expect(ctrl.loading).to.be.true;
mocks.tgLikeProjectButton.unlike.withArgs(project.get('id')).resolve()
promise.finally () ->
expect(mocks.tgLikeProjectButton.unlike).to.be.calledOnce
expect(ctrl.loading).to.be.false;
done()
it "toggleLike true -> false, notify error", (done) ->
project = Immutable.fromJS({
is_fan: true
})
ctrl = $controller("LikeProjectButton")
ctrl.project = project
mocks.tgLikeProjectButton.unlike.withArgs(project.get('id')).promise().reject()
ctrl.toggleLike().finally () ->
expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce
done()

View File

@ -0,0 +1,12 @@
LikeProjectButtonDirective = ->
return {
scope: {}
controller: "LikeProjectButton",
bindToController: {
project: '='
}
controllerAs: "vm",
templateUrl: "projects/components/like-project-button/like-project-button.html",
}
angular.module("taigaProjects").directive("tgLikeProjectButton", LikeProjectButtonDirective)

View File

@ -0,0 +1,29 @@
a.track-button.like-button.like-container(
href="",
title="{{ 'PROJECT.LIKE_BUTTON.BUTTON_TITLE' | translate }}"
ng-click="vm.toggleLike()"
ng-class="{'active':vm.project.get('is_fan'), 'is-hover':vm.project.get('is_fan') && vm.isMouseOver}"
ng-mouseover="vm.showTextWhenMouseIsOver()"
ng-mouseleave="vm.showTextWhenMouseIsLeave()"
)
span.track-inner
span.track-icon
include ../../../../svg/like.svg
span(
ng-if="!vm.project.get('is_fan')"
translate="PROJECT.LIKE_BUTTON.LIKE"
)
span(
ng-if="vm.project.get('is_fan') && !vm.isMouseOver"
translate="PROJECT.LIKE_BUTTON.LIKED"
)
span(
ng-if="vm.project.get('is_fan') && vm.isMouseOver"
translate="PROJECT.LIKE_BUTTON.UNLIKE"
)
span.track-button-counter(
title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_fans\")||0}:'messageformat' }}",
tg-loading="vm.loading"
)
| {{ vm.project.get('total_fans') }}

View File

@ -0,0 +1,52 @@
taiga = @.taiga
class LikeProjectButtonService extends taiga.Service
@.$inject = ["tgResources", "tgCurrentUserService", "tgProjectService"]
constructor: (@rs, @currentUserService, @projectService) ->
_getProjectIndex: (projectId) ->
return @currentUserService.projects
.get('all')
.findIndex (project) -> project.get('id') == projectId
_updateProjects: (projectId, isFan) ->
projectIndex = @._getProjectIndex(projectId)
projects = @currentUserService.projects
.get('all')
.update projectIndex, (project) ->
totalFans = project.get("total_fans")
if isFan then totalFans++ else totalFans--
return project.merge({
is_fan: isFan,
total_fans: totalFans
})
@currentUserService.setProjects(projects)
_updateCurrentProject: (isFan) ->
totalFans = @projectService.project.get("total_fans")
if isFan then totalFans++ else totalFans--
project = @projectService.project.merge({
is_fan: isFan,
total_fans: totalFans
})
@projectService.setProject(project)
like: (projectId) ->
return @rs.projects.likeProject(projectId).then =>
@._updateProjects(projectId, true)
@._updateCurrentProject(true)
unlike: (projectId) ->
return @rs.projects.unlikeProject(projectId).then =>
@._updateProjects(projectId, false)
@._updateCurrentProject(false)
angular.module("taigaProjects").service("tgLikeProjectButtonService", LikeProjectButtonService)

View File

@ -0,0 +1,132 @@
describe "tgLikeProjectButtonService", ->
likeButtonService = null
provide = null
mocks = {}
_mockTgResources = () ->
mocks.tgResources = {
projects: {
likeProject: sinon.stub(),
unlikeProject: sinon.stub()
}
}
provide.value "tgResources", mocks.tgResources
_mockTgCurrentUserService = () ->
mocks.tgCurrentUserService = {
setProjects: sinon.stub(),
projects: Immutable.fromJS({
all: [
{
id: 4,
total_fans: 2,
is_fan: false
},
{
id: 5,
total_fans: 7,
is_fan: true
},
{
id: 6,
total_fans: 4,
is_fan: true
}
]
})
}
provide.value "tgCurrentUserService", mocks.tgCurrentUserService
_mockTgProjectService = () ->
mocks.tgProjectService = {
setProject: sinon.stub()
}
provide.value "tgProjectService", mocks.tgProjectService
_inject = (callback) ->
inject (_tgLikeProjectButtonService_) ->
likeButtonService = _tgLikeProjectButtonService_
callback() if callback
_mocks = () ->
module ($provide) ->
provide = $provide
_mockTgResources()
_mockTgCurrentUserService()
_mockTgProjectService()
return null
_setup = ->
_mocks()
beforeEach ->
module "taigaProjects"
_setup()
_inject()
it "like", (done) ->
projectId = 4
mocks.tgResources.projects.likeProject.withArgs(projectId).promise().resolve()
newProject = {
id: 4,
total_fans: 3,
is_fan: true
}
mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 0])
userServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable[0], newProject)
), 'userServiceCheckImmutable'
projectServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable, newProject)
), 'projectServiceCheckImmutable'
likeButtonService.like(projectId).finally () ->
expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable)
expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable)
done()
it "unlike", (done) ->
projectId = 5
mocks.tgResources.projects.unlikeProject.withArgs(projectId).promise().resolve()
newProject = {
id: 5,
total_fans: 6,
is_fan: false
}
mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 1])
userServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable[1], newProject)
), 'userServiceCheckImmutable'
projectServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable, newProject)
), 'projectServiceCheckImmutable'
likeButtonService.unlike(projectId).finally () ->
expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable)
expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable)
done()

View File

@ -0,0 +1,33 @@
class WatchProjectButtonController
@.$inject = [
"$tgConfirm"
"tgWatchProjectButtonService"
]
constructor: (@confirm, @watchButtonService)->
@.showWatchOptions = false
@.loading = false
toggleWatcherOptions: () ->
@.showWatchOptions = !@.showWatchOptions
closeWatcherOptions: () ->
@.showWatchOptions = false
watch: (notifyLevel) ->
@.loading = true
@.closeWatcherOptions()
return @watchButtonService.watch(@.project.get('id'), notifyLevel)
.catch () => @confirm.notify("error")
.finally () => @.loading = false
unwatch: ->
@.loading = true
@.closeWatcherOptions()
return @watchButtonService.unwatch(@.project.get('id'))
.catch () => @confirm.notify("error")
.finally () => @.loading = false
angular.module("taigaProjects").controller("WatchProjectButton", WatchProjectButtonController)

View File

@ -0,0 +1,136 @@
describe "WatchProjectButton", ->
$provide = null
$controller = null
mocks = {}
_mockTgConfirm = ->
mocks.tgConfirm = {
notify: sinon.stub()
}
$provide.value("$tgConfirm", mocks.tgConfirm)
_mockTgWatchProjectButton = ->
mocks.tgWatchProjectButton = {
watch: sinon.stub(),
unwatch: sinon.stub()
}
$provide.value("tgWatchProjectButtonService", mocks.tgWatchProjectButton)
_mocks = ->
module (_$provide_) ->
$provide = _$provide_
_mockTgConfirm()
_mockTgWatchProjectButton()
return null
_inject = ->
inject (_$controller_) ->
$controller = _$controller_
_setup = ->
_mocks()
_inject()
beforeEach ->
module "taigaProjects"
_setup()
it "toggleWatcherOption", () ->
ctrl = $controller("WatchProjectButton")
ctrl.toggleWatcherOptions()
expect(ctrl.showWatchOptions).to.be.true
ctrl.toggleWatcherOptions()
expect(ctrl.showWatchOptions).to.be.false
it "watch", (done) ->
notifyLevel = 5
project = Immutable.fromJS({
id: 3
})
ctrl = $controller("WatchProjectButton")
ctrl.project = project
ctrl.showWatchOptions = true
mocks.tgWatchProjectButton.watch = sinon.stub().promise()
promise = ctrl.watch(notifyLevel)
expect(ctrl.loading).to.be.true
mocks.tgWatchProjectButton.watch.withArgs(project.get('id'), notifyLevel).resolve()
promise.finally () ->
expect(mocks.tgWatchProjectButton.watch).to.be.calledOnce
expect(ctrl.showWatchOptions).to.be.false
expect(ctrl.loading).to.be.false
done()
it "watch, notify error", (done) ->
notifyLevel = 5
project = Immutable.fromJS({
id: 3
})
ctrl = $controller("WatchProjectButton")
ctrl.project = project
ctrl.showWatchOptions = true
mocks.tgWatchProjectButton.watch.withArgs(project.get('id'), notifyLevel).promise().reject()
ctrl.watch(notifyLevel).finally () ->
expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce
expect(ctrl.showWatchOptions).to.be.false
expect(ctrl.loading).to.be.false
done()
it "unwatch", (done) ->
project = Immutable.fromJS({
id: 3
})
ctrl = $controller("WatchProjectButton")
ctrl.project = project
ctrl.showWatchOptions = true
mocks.tgWatchProjectButton.unwatch = sinon.stub().promise()
promise = ctrl.unwatch()
expect(ctrl.loading).to.be.true
mocks.tgWatchProjectButton.unwatch.withArgs(project.get('id')).resolve()
promise.finally () ->
expect(mocks.tgWatchProjectButton.unwatch).to.be.calledOnce
expect(ctrl.showWatchOptions).to.be.false
done()
it "unwatch, notify error", (done) ->
project = Immutable.fromJS({
id: 3
})
ctrl = $controller("WatchProjectButton")
ctrl.project = project
ctrl.showWatchOptions = true
mocks.tgWatchProjectButton.unwatch.withArgs(project.get('id')).promise().reject()
ctrl.unwatch().finally () ->
expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce
expect(ctrl.showWatchOptions).to.be.false
done()

View File

@ -0,0 +1,12 @@
WatchProjectButtonDirective = ->
return {
scope: {}
controller: "WatchProjectButton",
bindToController: {
project: "="
}
controllerAs: "vm",
templateUrl: "projects/components/watch-project-button/watch-project-button.html",
}
angular.module("taigaProjects").directive("tgWatchProjectButton", WatchProjectButtonDirective)

View File

@ -0,0 +1,56 @@
a.track-button.watch-button.watch-container(
href="",
title="{{ 'PROJECT.WATCH_BUTTON.BUTTON_TITLE' | translate }}"
ng-click="vm.toggleWatcherOptions()"
ng-class="{'active': vm.project.get('is_watcher')}"
)
span.track-inner
span.track-icon
include ../../../../svg/watch.svg
span(ng-if="!vm.project.get('is_watcher')", translate="PROJECT.WATCH_BUTTON.WATCH")
span(ng-if="vm.project.get('is_watcher')", translate="PROJECT.WATCH_BUTTON.WATCHING")
span.icon.icon-arrow-up
span.track-button-counter(
title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_watchers\")||0}:'messageformat' }}",
tg-loading="vm.loading"
)
| {{ vm.project.get('total_watchers') }}
ul.watch-options(
ng-class="{'hidden': !vm.showWatchOptions}"
ng-mouseleave="vm.closeWatcherOptions()"
)
//- NOTIFY LEVEL CHOICES:
//- 1 - Only involved
//- 2 - Receive all
//- 3 - No notifications
li
a(
href="",
title="{{ 'PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_ALL_TITLE' | translate }}",
ng-click="vm.watch(2)",
ng-class="{'active': vm.project.get('is_watcher') && vm.project.get('notify_level') == 2}"
)
span(translate="PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_ALL")
span.watch-check(ng-if="vm.project.get('is_watcher') && vm.project.get('notify_level') == 2")
include ../../../../svg/check.svg
li
a(
href="",
title="{{ 'PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_INVOLVED_TITLE' | translate }}",
ng-click="vm.watch(1)",
ng-class="{'active': vm.project.get('is_watcher') && vm.project.get('notify_level') == 1}"
)
span(translate="PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_INVOLVED")
span.watch-check(ng-if="vm.project.get('is_watcher') && vm.project.get('notify_level') == 1")
include ../../../../svg/check.svg
li(ng-if="vm.project.get('is_watcher')")
a(
href="",
title="{{ 'PROJECT.WATCH_BUTTON.OPTIONS.UNWATCH_TITLE' | translate }}",
ng-click="vm.unwatch()"
)
span(translate="PROJECT.WATCH_BUTTON.OPTIONS.UNWATCH")

View File

@ -0,0 +1,59 @@
taiga = @.taiga
class WatchProjectButtonService extends taiga.Service
@.$inject = [
"tgResources",
"tgCurrentUserService",
"tgProjectService"
]
constructor: (@rs, @currentUserService, @projectService) ->
_getProjectIndex: (projectId) ->
return @currentUserService.projects
.get('all')
.findIndex (project) -> project.get('id') == projectId
_updateProjects: (projectId, notifyLevel, isWatcher) ->
projectIndex = @._getProjectIndex(projectId)
projects = @currentUserService.projects
.get('all')
.update projectIndex, (project) =>
totalWatchers = project.get('total_watchers')
if isWatcher then totalWatchers++ else totalWatchers--
return project.merge({
is_watcher: isWatcher,
total_watchers: totalWatchers
notify_level: notifyLevel
})
@currentUserService.setProjects(projects)
_updateCurrentProject: (notifyLevel, isWatcher) ->
totalWatchers = @projectService.project.get("total_watchers")
if isWatcher then totalWatchers++ else totalWatchers--
project = @projectService.project.merge({
is_watcher: isWatcher,
total_watchers: totalWatchers
notify_level: notifyLevel
})
@projectService.setProject(project)
watch: (projectId, notifyLevel) ->
return @rs.projects.watchProject(projectId, notifyLevel).then =>
@._updateProjects(projectId, notifyLevel, true)
@._updateCurrentProject(notifyLevel, true)
unwatch: (projectId) ->
return @rs.projects.unwatchProject(projectId).then =>
@._updateProjects(projectId, null, false)
@._updateCurrentProject(null, false)
angular.module("taigaProjects").service("tgWatchProjectButtonService", WatchProjectButtonService)

View File

@ -0,0 +1,142 @@
describe "tgWatchProjectButtonService", ->
watchButtonService = null
provide = null
mocks = {}
_mockTgResources = () ->
mocks.tgResources = {
projects: {
watchProject: sinon.stub(),
unwatchProject: sinon.stub()
}
}
provide.value "tgResources", mocks.tgResources
_mockTgCurrentUserService = () ->
mocks.tgCurrentUserService = {
setProjects: sinon.stub(),
getUser: () ->
return Immutable.fromJS({
id: 89
})
projects: Immutable.fromJS({
all: [
{
id: 4,
total_watchers: 0,
is_watcher: false,
notify_level: null
},
{
id: 5,
total_watchers: 1,
is_watcher: true,
notify_level: 3
},
{
id: 6,
total_watchers: 0,
is_watcher: true,
notify_level: null
}
]
})
}
provide.value "tgCurrentUserService", mocks.tgCurrentUserService
_mockTgProjectService = () ->
mocks.tgProjectService = {
setProject: sinon.stub()
}
provide.value "tgProjectService", mocks.tgProjectService
_inject = (callback) ->
inject (_tgWatchProjectButtonService_) ->
watchButtonService = _tgWatchProjectButtonService_
callback() if callback
_mocks = () ->
module ($provide) ->
provide = $provide
_mockTgResources()
_mockTgCurrentUserService()
_mockTgProjectService()
return null
_setup = ->
_mocks()
beforeEach ->
module "taigaProjects"
_setup()
_inject()
it "watch", (done) ->
projectId = 4
notifyLevel = 3
mocks.tgResources.projects.watchProject.withArgs(projectId, notifyLevel).promise().resolve()
newProject = {
id: 4,
total_watchers: 1,
is_watcher: true,
notify_level: notifyLevel
}
mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 0])
userServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable[0], newProject)
), 'userServiceCheckImmutable'
projectServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable, newProject)
), 'projectServiceCheckImmutable'
watchButtonService.watch(projectId, notifyLevel).finally () ->
expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable)
expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable)
done()
it "unwatch", (done) ->
projectId = 5
mocks.tgResources.projects.unwatchProject.withArgs(projectId).promise().resolve()
newProject = {
id: 5,
total_watchers: 0,
is_watcher: false,
notify_level: null
}
mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 1])
userServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable[1], newProject)
), 'userServiceCheckImmutable'
projectServiceCheckImmutable = sinon.match ((immutable) ->
immutable = immutable.toJS()
return _.isEqual(immutable, newProject)
), 'projectServiceCheckImmutable'
watchButtonService.unwatch(projectId).finally () ->
expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable)
expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable)
done()

View File

@ -11,17 +11,17 @@ div.project-list-wrapper.centered
section.project-list-section
div.project-list
ul(tg-sort-projects="vm.projects")
li.project-list-single(tg-bind-scope, tg-repeat="project in vm.projects track by project.get('id')")
div.project-list-single-left
div.project-title
h1.project-name
li.list-itemtype-project(tg-bind-scope, tg-repeat="project in vm.projects track by project.get('id')")
div.list-itemtype-project-left
div.list-itemtype-project-data
h2
a(href="#", tg-nav="project:project=project.get('slug')", title="{{ ::project.get('name') }}") {{project.get('name')}}
span.private(ng-if="project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
include ../../../svg/lock.svg
p {{ ::project.get('description') | limitTo:300 }}
span(ng-if="::project.get('description').length > 300") ...
div.project-list-single-tags.tags-container(ng-if="::project.get('tags').size")
span.private(ng-if="project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
include ../../../svg/lock.svg
p {{ ::project.get('description') | limitTo:300 }}
span(ng-if="::project.get('description').length > 300") ...
div.list-itemtype-project-tags.tag-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')}}

View File

@ -1,80 +0,0 @@
.project-list-single {
border-bottom: 1px solid $whitish;
display: flex;
justify-content: space-between;
min-height: 9rem;
padding: 1rem;
position: relative;
}
.project-list-single-left {
display: flex;
flex-direction: column;
padding-right: 1rem;
h1 {
@extend %text;
@extend %larger;
color: $gray;
display: inline-block;
margin-bottom: 0;
text-transform: none;
vertical-align: middle;
white-space: nowrap;
}
p {
@extend %text;
@extend %xsmall;
color: $gray;
margin-bottom: 0;
}
.project-list-single-tags {
align-self: flex-end;
display: flex;
flex: 3;
flex-wrap: wrap;
margin-top: .5rem;
}
.tag {
align-self: flex-end;
margin-right: .5rem;
padding: .5rem;
}
}
.project-list-single-right {
flex-shrink: 0;
justify-content: space-between;
width: 200px;
.project-list-single-stats {
align-self: flex-end;
display: flex;
div {
color: $gray-light;
margin-right: .5rem;
.icon {
margin-right: .2rem;
vertical-align: center;
}
}
.active {
.icon {
color: $primary-light;
}
}
}
.project-list-single-members {
align-self: flex-end;
display: flex;
flex-direction: row-reverse;
flex-grow: 0;
flex-wrap: wrap-reverse;
margin-top: 1rem;
a {
display: block; }
img {
border-radius: .1rem;
margin-right: .3rem;
width: 34px;
}
}
}

View File

@ -1,10 +1,13 @@
.profile-projects {
border-top: 1px solid $whitish;
.project-list-single {
.list-itemtype-project {
display: flex;
justify-content: space-between;
min-height: 10rem;
.list-itemtype-project-right {
display: flex;
flex-direction: column;
width: 200px;
}
}
}

View File

@ -9,6 +9,7 @@
padding: .9rem 1rem;
h1 {
@extend %larger;
@extend %light;
margin: 0;
}
}
@ -41,13 +42,12 @@
}
.placeholder {
background-color: lighten($whitish, 3%);
height: 7rem;
width: 100%;
height: 5rem;
}
.project-list-single {
background: $white;
.list-itemtype-project {
background: rgba($white, .6);
&:hover {
background: lighten($primary, 60%);
background: lighten($primary, 63%);
cursor: move;
transition: background .3s;
.drag {

View File

@ -1,36 +1,31 @@
class ProjectController
@.$inject = [
"tgProjectsService",
"$routeParams",
"tgAppMetaService",
"$tgAuth",
"tgXhrErrorService",
"$translate"
"$translate",
"tgProjectService"
]
constructor: (@projectsService, @routeParams, @appMetaService, @auth, @xhrError, @translate) ->
constructor: (@routeParams, @appMetaService, @auth, @translate, @projectService) ->
projectSlug = @routeParams.pslug
@.user = @auth.userData
@projectsService
.getProjectBySlug(projectSlug)
.then (project) =>
@.project = project
taiga.defineImmutableProperty @, "project", () => return @projectService.project
taiga.defineImmutableProperty @, "members", () => return @projectService.activeMembers
members = @.project.get('members').filter (member) -> member.get('is_active')
@.project = @.project.set('members', members)
@._setMeta(@.project)
.catch (xhr) =>
@xhrError.response(xhr)
@appMetaService.setfn @._setMeta.bind(this)
_setMeta: (project)->
ctx = {projectName: project.get("name")}
metas = {}
title = @translate.instant("PROJECT.PAGE_TITLE", ctx)
description = project.get("description")
@appMetaService.setAll(title, description)
return metas if !@.project
ctx = {projectName: @.project.get("name")}
metas.title = @translate.instant("PROJECT.PAGE_TITLE", ctx)
metas.description = @.project.get("description")
return metas
angular.module("taigaProjects").controller("Project", ProjectController)

View File

@ -5,16 +5,14 @@ describe "ProjectController", ->
$rootScope = null
mocks = {}
_mockProjectsService = () ->
mocks.projectService = {
getProjectBySlug: sinon.stub()
}
_mockProjectService = () ->
mocks.projectService = {}
provide.value "tgProjectsService", mocks.projectService
provide.value "tgProjectService", mocks.projectService
_mockAppMetaService = () ->
mocks.appMetaService = {
setAll: sinon.stub()
setfn: sinon.stub()
}
provide.value "tgAppMetaService", mocks.appMetaService
@ -31,13 +29,6 @@ describe "ProjectController", ->
pslug: "project-slug"
}
_mockXhrErrorService = () ->
mocks.xhrErrorService = {
response: sinon.spy()
}
provide.value "tgXhrErrorService", mocks.xhrErrorService
_mockTranslate = () ->
mocks.translate = {}
mocks.translate.instant = sinon.stub()
@ -47,11 +38,10 @@ describe "ProjectController", ->
_mocks = () ->
module ($provide) ->
provide = $provide
_mockProjectsService()
_mockProjectService()
_mockRouteParams()
_mockAppMetaService()
_mockAuth()
_mockXhrErrorService()
_mockTranslate()
return null
@ -72,14 +62,12 @@ describe "ProjectController", ->
members: []
})
mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project)
ctrl = $controller "Project",
$scope: {}
expect(ctrl.user).to.be.equal(mocks.auth.userData)
it "set page title", (done) ->
it "set page title", () ->
$scope = $rootScope.$new()
project = Immutable.fromJS({
name: "projectName"
@ -93,44 +81,31 @@ describe "ProjectController", ->
})
.returns('projectTitle')
mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project)
mocks.projectService.project = project
ctrl = $controller("Project")
setTimeout ( ->
expect(mocks.appMetaService.setAll.calledWithExactly("projectTitle", "projectDescription")).to.be.true
done()
)
metas = ctrl._setMeta(project)
it "set local project variable with active members", (done) ->
expect(metas.title).to.be.equal('projectTitle')
expect(metas.description).to.be.equal('projectDescription')
expect(mocks.appMetaService.setfn).to.be.calledOnce
it "set local project variable and members", () ->
project = Immutable.fromJS({
name: "projectName",
members: [
{is_active: true},
{is_active: true},
{is_active: true},
{is_active: false}
]
name: "projectName"
})
mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project)
members = Immutable.fromJS([
{is_active: true},
{is_active: true},
{is_active: true}
])
mocks.projectService.project = project
mocks.projectService.activeMembers = members
ctrl = $controller("Project")
setTimeout (() ->
expect(ctrl.project.get('members').size).to.be.equal(3)
done()
)
it "handle project error", (done) ->
xhr = {code: 403}
mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().reject(xhr)
ctrl = $controller("Project")
setTimeout (() ->
expect(mocks.xhrErrorService.response.withArgs(xhr)).to.be.calledOnce
done()
)
expect(ctrl.project).to.be.equal(project)
expect(ctrl.members).to.be.equal(members)

View File

@ -2,29 +2,64 @@ div.wrapper
tg-project-menu
div.centered.single-project
section.single-project-intro
h1
span.green(class="project-name") {{::vm.project.get("name")}}
span.private(ng-if="::vm.project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
include ../../../svg/lock.svg
div.intro-options
h1
span.project-name {{::vm.project.get("name")}}
span.private(
ng-if="::vm.project.get('is_private')"
title="{{'PROJECT.PRIVATE' | translate}}"
)
include ../../../svg/lock.svg
//- Like and wacht buttons for authenticated users
div.track-buttons-container(ng-if="vm.user")
tg-like-project-button(project="vm.project")
tg-watch-project-button(project="vm.project")
//- Like and wacht buttons for anonymous users
div.track-container(ng-if="!vm.user")
.list-itemtype-track
span.list-itemtype-track-likers(
title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_fans\")||0}:'messageformat' }}"
)
span.icon
include ../../../svg/like.svg
span {{ ::vm.project.get('total_fans') }}
span.list-itemtype-track-watchers(
title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_watchers\")||0}:'messageformat' }}"
)
span.icon
include ../../../svg/watch.svg
span {{ ::vm.project.get('total_watchers') }}
p.description {{vm.project.get('description')}}
div.single-project-tags.tags-container(ng-if="::vm.project.get('tags').size")
span.tag(style='border-left: 5px solid {{::tag.get("color")}};',
tg-repeat="tag in ::vm.project.get('colorized_tags')")
span.tag(
style='border-left: 5px solid {{::tag.get("color")}};',
tg-repeat="tag in ::vm.project.get('colorized_tags')"
)
span.tag-name {{::tag.get('name')}}
div.project-data
section.timeline(ng-if="vm.project")
div(tg-user-timeline, projectId="vm.project.get('id')")
section.involved-data
h2.title {{"PROJECT.SECTION.TEAM" | translate}}
ul.involved-team
li(tg-repeat="member in ::vm.project.get('members')")
a(tg-nav="user-profile:username=member.get('username')",
title="{{::member.get('full_name')}}")
img(ng-src="{{::member.get('photo')}}", alt="{{::member.get('full_name')}}")
// h2.title Organizations
// div.involved-organization
// a(href="", title="User Name")
// img(src="https://s3.amazonaws.com/uifaces/faces/twitter/dan_higham/48.jpg", alt="{{member.full_name}}")
li(tg-repeat="member in vm.members")
a(
tg-nav="user-profile:username=member.get('username')",
title="{{::member.get('full_name')}}"
)
img(ng-src="{{::member.get('photo')}}", alt="{{::member.get('full_name')}}")
//-
h2.title Organizations
div.involved-organization
a(href="", title="User Name")
img(
src="https://s3.amazonaws.com/uifaces/faces/twitter/dan_higham/48.jpg"
alt="{{member.full_name}}"
)

View File

@ -2,17 +2,20 @@ taiga = @.taiga
class ProjectService
@.$inject = [
"tgProjectsService"
"tgProjectsService",
"tgXhrErrorService"
]
constructor: (@projectsService) ->
constructor: (@projectsService, @xhrError) ->
@._project = null
@._section = null
@._sectionsBreadcrumb = Immutable.List()
@._activeMembers = Immutable.List()
taiga.defineImmutableProperty @, "project", () => return @._project
taiga.defineImmutableProperty @, "section", () => return @._section
taiga.defineImmutableProperty @, "sectionsBreadcrumb", () => return @._sectionsBreadcrumb
taiga.defineImmutableProperty @, "activeMembers", () => return @._activeMembers
setSection: (section) ->
@._section = section
@ -22,20 +25,32 @@ class ProjectService
else
@._sectionsBreadcrumb = Immutable.List()
setProject: (pslug) ->
if @._pslug != pslug
@._pslug = pslug
setProjectBySlug: (pslug) ->
return new Promise (resolve, reject) =>
if !@.project || @.project.get('slug') != pslug
@projectsService
.getProjectBySlug(pslug)
.then (project) =>
@.setProject(project)
resolve()
.catch (xhr) =>
@xhrError.response(xhr)
@.fetchProject()
else resolve()
setProject: (project) ->
@._project = project
@._activeMembers = @._project.get('members').filter (member) -> member.get('is_active')
cleanProject: () ->
@._pslug = null
@._project = null
@._activeMembers = Immutable.List()
@._section = null
@._sectionsBreadcrumb = Immutable.List()
fetchProject: () ->
return @projectsService.getProjectBySlug(@._pslug).then (project) =>
@._project = project
pslug = @.project.get('slug')
return @projectsService.getProjectBySlug(pslug).then (project) => @.setProject(project)
angular.module("taigaCommon").service("tgProjectService", ProjectService)

View File

@ -10,11 +10,19 @@ describe "tgProjectService", ->
$provide.value "tgProjectsService", mocks.projectsService
_mockXhrErrorService = () ->
mocks.xhrErrorService = {
response: sinon.stub()
}
$provide.value "tgXhrErrorService", mocks.xhrErrorService
_mocks = () ->
module (_$provide_) ->
$provide = _$provide_
_mockProjectsService()
_mockXhrErrorService()
return null
@ -46,31 +54,70 @@ describe "tgProjectService", ->
expect(projectService.sectionsBreadcrumb.toJS()).to.be.eql(breadcrumb)
it "set project if the project slug has changed", () ->
projectService.fetchProject = sinon.spy()
it "set project if the project slug has changed", (done) ->
projectService.setProject = sinon.spy()
pslug = "slug-1"
project = Immutable.Map({
id: 1,
slug: 'slug-1',
members: []
})
projectService.setProject(pslug)
mocks.projectsService.getProjectBySlug.withArgs('slug-1').promise().resolve(project)
mocks.projectsService.getProjectBySlug.withArgs('slug-2').promise().resolve(project)
expect(projectService.fetchProject).to.be.calledOnce
projectService.setProjectBySlug('slug-1')
.then () -> projectService.setProjectBySlug('slug-1')
.then () -> projectService.setProjectBySlug('slug-2')
.finally () ->
expect(projectService.setProject).to.be.called.twice;
done()
projectService.setProject(pslug)
it "set project and set active members", () ->
project = Immutable.fromJS({
name: 'test project',
members: [
{is_active: true},
{is_active: false},
{is_active: true},
{is_active: false},
{is_active: false}
]
})
expect(projectService.fetchProject).to.be.calledOnce
projectService.setProject(project)
projectService.setProject("slug-2")
expect(projectService.fetchProject).to.be.calledTwice
expect(projectService.project).to.be.equal(project)
expect(projectService.activeMembers.size).to.be.equal(2)
it "fetch project", (done) ->
project = Immutable.Map({id: 1})
pslug = "slug-1"
project = Immutable.Map({
id: 1,
slug: 'slug',
members: []
})
projectService._pslug = pslug
projectService._project = project
mocks.projectsService.getProjectBySlug.withArgs(pslug).promise().resolve(project)
mocks.projectsService.getProjectBySlug.withArgs(project.get('slug')).promise().resolve(project)
projectService.fetchProject().then () ->
expect(projectService.project).to.be.equal(project)
done()
it "clean project", () ->
projectService._section = "fakeSection"
projectService._sectionsBreadcrumb = ["fakeSection"]
projectService._activeMembers = ["fakeMember"]
projectService._project = Immutable.Map({
id: 1,
slug: 'slug',
members: []
})
projectService.cleanProject()
expect(projectService.project).to.be.null;
expect(projectService.activeMembers.size).to.be.equal(0);
expect(projectService.section).to.be.null;
expect(projectService.sectionsBreadcrumb.size).to.be.equal(0);