Create like and watch buttons in project detail pages
parent
d1349c4272
commit
f52935f8c9
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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') }}
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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")
|
|
@ -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)
|
|
@ -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()
|
|
@ -11,17 +11,17 @@ div.project-list-wrapper.centered
|
||||||
section.project-list-section
|
section.project-list-section
|
||||||
div.project-list
|
div.project-list
|
||||||
ul(tg-sort-projects="vm.projects")
|
ul(tg-sort-projects="vm.projects")
|
||||||
li.project-list-single(tg-bind-scope, tg-repeat="project in vm.projects track by project.get('id')")
|
li.list-itemtype-project(tg-bind-scope, tg-repeat="project in vm.projects track by project.get('id')")
|
||||||
div.project-list-single-left
|
div.list-itemtype-project-left
|
||||||
div.project-title
|
div.list-itemtype-project-data
|
||||||
h1.project-name
|
h2
|
||||||
a(href="#", tg-nav="project:project=project.get('slug')", title="{{ ::project.get('name') }}") {{project.get('name')}}
|
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}}")
|
span.private(ng-if="project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
|
||||||
include ../../../svg/lock.svg
|
include ../../../svg/lock.svg
|
||||||
p {{ ::project.get('description') | limitTo:300 }}
|
p {{ ::project.get('description') | limitTo:300 }}
|
||||||
span(ng-if="::project.get('description').length > 300") ...
|
span(ng-if="::project.get('description').length > 300") ...
|
||||||
|
|
||||||
div.project-list-single-tags.tags-container(ng-if="::project.get('tags').size")
|
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(style='border-left: 5px solid {{::tag.get("color")}};', tg-repeat="tag in ::project.get('colorized_tags')")
|
||||||
span.tag-name {{::tag.get('name')}}
|
span.tag-name {{::tag.get('name')}}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,13 @@
|
||||||
.profile-projects {
|
.profile-projects {
|
||||||
border-top: 1px solid $whitish;
|
border-top: 1px solid $whitish;
|
||||||
.project-list-single {
|
.list-itemtype-project {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 10rem;
|
min-height: 10rem;
|
||||||
|
.list-itemtype-project-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
padding: .9rem 1rem;
|
padding: .9rem 1rem;
|
||||||
h1 {
|
h1 {
|
||||||
@extend %larger;
|
@extend %larger;
|
||||||
|
@extend %light;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,13 +42,12 @@
|
||||||
}
|
}
|
||||||
.placeholder {
|
.placeholder {
|
||||||
background-color: lighten($whitish, 3%);
|
background-color: lighten($whitish, 3%);
|
||||||
height: 7rem;
|
height: 5rem;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.project-list-single {
|
.list-itemtype-project {
|
||||||
background: $white;
|
background: rgba($white, .6);
|
||||||
&:hover {
|
&:hover {
|
||||||
background: lighten($primary, 60%);
|
background: lighten($primary, 63%);
|
||||||
cursor: move;
|
cursor: move;
|
||||||
transition: background .3s;
|
transition: background .3s;
|
||||||
.drag {
|
.drag {
|
||||||
|
|
|
@ -1,36 +1,31 @@
|
||||||
class ProjectController
|
class ProjectController
|
||||||
@.$inject = [
|
@.$inject = [
|
||||||
"tgProjectsService",
|
|
||||||
"$routeParams",
|
"$routeParams",
|
||||||
"tgAppMetaService",
|
"tgAppMetaService",
|
||||||
"$tgAuth",
|
"$tgAuth",
|
||||||
"tgXhrErrorService",
|
"$translate",
|
||||||
"$translate"
|
"tgProjectService"
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor: (@projectsService, @routeParams, @appMetaService, @auth, @xhrError, @translate) ->
|
constructor: (@routeParams, @appMetaService, @auth, @translate, @projectService) ->
|
||||||
projectSlug = @routeParams.pslug
|
projectSlug = @routeParams.pslug
|
||||||
@.user = @auth.userData
|
@.user = @auth.userData
|
||||||
|
|
||||||
@projectsService
|
taiga.defineImmutableProperty @, "project", () => return @projectService.project
|
||||||
.getProjectBySlug(projectSlug)
|
taiga.defineImmutableProperty @, "members", () => return @projectService.activeMembers
|
||||||
.then (project) =>
|
|
||||||
@.project = project
|
|
||||||
|
|
||||||
members = @.project.get('members').filter (member) -> member.get('is_active')
|
@appMetaService.setfn @._setMeta.bind(this)
|
||||||
|
|
||||||
@.project = @.project.set('members', members)
|
|
||||||
|
|
||||||
@._setMeta(@.project)
|
|
||||||
|
|
||||||
.catch (xhr) =>
|
|
||||||
@xhrError.response(xhr)
|
|
||||||
|
|
||||||
_setMeta: (project)->
|
_setMeta: (project)->
|
||||||
ctx = {projectName: project.get("name")}
|
metas = {}
|
||||||
|
|
||||||
title = @translate.instant("PROJECT.PAGE_TITLE", ctx)
|
return metas if !@.project
|
||||||
description = project.get("description")
|
|
||||||
@appMetaService.setAll(title, description)
|
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)
|
angular.module("taigaProjects").controller("Project", ProjectController)
|
||||||
|
|
|
@ -5,16 +5,14 @@ describe "ProjectController", ->
|
||||||
$rootScope = null
|
$rootScope = null
|
||||||
mocks = {}
|
mocks = {}
|
||||||
|
|
||||||
_mockProjectsService = () ->
|
_mockProjectService = () ->
|
||||||
mocks.projectService = {
|
mocks.projectService = {}
|
||||||
getProjectBySlug: sinon.stub()
|
|
||||||
}
|
|
||||||
|
|
||||||
provide.value "tgProjectsService", mocks.projectService
|
provide.value "tgProjectService", mocks.projectService
|
||||||
|
|
||||||
_mockAppMetaService = () ->
|
_mockAppMetaService = () ->
|
||||||
mocks.appMetaService = {
|
mocks.appMetaService = {
|
||||||
setAll: sinon.stub()
|
setfn: sinon.stub()
|
||||||
}
|
}
|
||||||
|
|
||||||
provide.value "tgAppMetaService", mocks.appMetaService
|
provide.value "tgAppMetaService", mocks.appMetaService
|
||||||
|
@ -31,13 +29,6 @@ describe "ProjectController", ->
|
||||||
pslug: "project-slug"
|
pslug: "project-slug"
|
||||||
}
|
}
|
||||||
|
|
||||||
_mockXhrErrorService = () ->
|
|
||||||
mocks.xhrErrorService = {
|
|
||||||
response: sinon.spy()
|
|
||||||
}
|
|
||||||
|
|
||||||
provide.value "tgXhrErrorService", mocks.xhrErrorService
|
|
||||||
|
|
||||||
_mockTranslate = () ->
|
_mockTranslate = () ->
|
||||||
mocks.translate = {}
|
mocks.translate = {}
|
||||||
mocks.translate.instant = sinon.stub()
|
mocks.translate.instant = sinon.stub()
|
||||||
|
@ -47,11 +38,10 @@ describe "ProjectController", ->
|
||||||
_mocks = () ->
|
_mocks = () ->
|
||||||
module ($provide) ->
|
module ($provide) ->
|
||||||
provide = $provide
|
provide = $provide
|
||||||
_mockProjectsService()
|
_mockProjectService()
|
||||||
_mockRouteParams()
|
_mockRouteParams()
|
||||||
_mockAppMetaService()
|
_mockAppMetaService()
|
||||||
_mockAuth()
|
_mockAuth()
|
||||||
_mockXhrErrorService()
|
|
||||||
_mockTranslate()
|
_mockTranslate()
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
@ -72,14 +62,12 @@ describe "ProjectController", ->
|
||||||
members: []
|
members: []
|
||||||
})
|
})
|
||||||
|
|
||||||
mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project)
|
|
||||||
|
|
||||||
ctrl = $controller "Project",
|
ctrl = $controller "Project",
|
||||||
$scope: {}
|
$scope: {}
|
||||||
|
|
||||||
expect(ctrl.user).to.be.equal(mocks.auth.userData)
|
expect(ctrl.user).to.be.equal(mocks.auth.userData)
|
||||||
|
|
||||||
it "set page title", (done) ->
|
it "set page title", () ->
|
||||||
$scope = $rootScope.$new()
|
$scope = $rootScope.$new()
|
||||||
project = Immutable.fromJS({
|
project = Immutable.fromJS({
|
||||||
name: "projectName"
|
name: "projectName"
|
||||||
|
@ -93,44 +81,31 @@ describe "ProjectController", ->
|
||||||
})
|
})
|
||||||
.returns('projectTitle')
|
.returns('projectTitle')
|
||||||
|
|
||||||
mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project)
|
mocks.projectService.project = project
|
||||||
|
|
||||||
ctrl = $controller("Project")
|
ctrl = $controller("Project")
|
||||||
|
|
||||||
setTimeout ( ->
|
metas = ctrl._setMeta(project)
|
||||||
expect(mocks.appMetaService.setAll.calledWithExactly("projectTitle", "projectDescription")).to.be.true
|
|
||||||
done()
|
|
||||||
)
|
|
||||||
|
|
||||||
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({
|
project = Immutable.fromJS({
|
||||||
name: "projectName",
|
name: "projectName"
|
||||||
members: [
|
|
||||||
{is_active: true},
|
|
||||||
{is_active: true},
|
|
||||||
{is_active: true},
|
|
||||||
{is_active: false}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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")
|
ctrl = $controller("Project")
|
||||||
|
|
||||||
setTimeout (() ->
|
expect(ctrl.project).to.be.equal(project)
|
||||||
expect(ctrl.project.get('members').size).to.be.equal(3)
|
expect(ctrl.members).to.be.equal(members)
|
||||||
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
|
@ -2,29 +2,64 @@ div.wrapper
|
||||||
tg-project-menu
|
tg-project-menu
|
||||||
div.centered.single-project
|
div.centered.single-project
|
||||||
section.single-project-intro
|
section.single-project-intro
|
||||||
h1
|
div.intro-options
|
||||||
span.green(class="project-name") {{::vm.project.get("name")}}
|
h1
|
||||||
span.private(ng-if="::vm.project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
|
span.project-name {{::vm.project.get("name")}}
|
||||||
include ../../../svg/lock.svg
|
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')}}
|
p.description {{vm.project.get('description')}}
|
||||||
|
|
||||||
div.single-project-tags.tags-container(ng-if="::vm.project.get('tags').size")
|
div.single-project-tags.tags-container(ng-if="::vm.project.get('tags').size")
|
||||||
span.tag(style='border-left: 5px solid {{::tag.get("color")}};',
|
span.tag(
|
||||||
tg-repeat="tag in ::vm.project.get('colorized_tags')")
|
style='border-left: 5px solid {{::tag.get("color")}};',
|
||||||
|
tg-repeat="tag in ::vm.project.get('colorized_tags')"
|
||||||
|
)
|
||||||
span.tag-name {{::tag.get('name')}}
|
span.tag-name {{::tag.get('name')}}
|
||||||
|
|
||||||
div.project-data
|
div.project-data
|
||||||
section.timeline(ng-if="vm.project")
|
section.timeline(ng-if="vm.project")
|
||||||
div(tg-user-timeline, projectId="vm.project.get('id')")
|
div(tg-user-timeline, projectId="vm.project.get('id')")
|
||||||
|
|
||||||
section.involved-data
|
section.involved-data
|
||||||
h2.title {{"PROJECT.SECTION.TEAM" | translate}}
|
h2.title {{"PROJECT.SECTION.TEAM" | translate}}
|
||||||
ul.involved-team
|
ul.involved-team
|
||||||
li(tg-repeat="member in ::vm.project.get('members')")
|
li(tg-repeat="member in vm.members")
|
||||||
a(tg-nav="user-profile:username=member.get('username')",
|
a(
|
||||||
title="{{::member.get('full_name')}}")
|
tg-nav="user-profile:username=member.get('username')",
|
||||||
img(ng-src="{{::member.get('photo')}}", alt="{{::member.get('full_name')}}")
|
title="{{::member.get('full_name')}}"
|
||||||
|
)
|
||||||
// h2.title Organizations
|
img(ng-src="{{::member.get('photo')}}", alt="{{::member.get('full_name')}}")
|
||||||
// div.involved-organization
|
//-
|
||||||
// a(href="", title="User Name")
|
h2.title Organizations
|
||||||
// img(src="https://s3.amazonaws.com/uifaces/faces/twitter/dan_higham/48.jpg", alt="{{member.full_name}}")
|
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}}"
|
||||||
|
)
|
||||||
|
|
|
@ -2,17 +2,20 @@ taiga = @.taiga
|
||||||
|
|
||||||
class ProjectService
|
class ProjectService
|
||||||
@.$inject = [
|
@.$inject = [
|
||||||
"tgProjectsService"
|
"tgProjectsService",
|
||||||
|
"tgXhrErrorService"
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor: (@projectsService) ->
|
constructor: (@projectsService, @xhrError) ->
|
||||||
@._project = null
|
@._project = null
|
||||||
@._section = null
|
@._section = null
|
||||||
@._sectionsBreadcrumb = Immutable.List()
|
@._sectionsBreadcrumb = Immutable.List()
|
||||||
|
@._activeMembers = Immutable.List()
|
||||||
|
|
||||||
taiga.defineImmutableProperty @, "project", () => return @._project
|
taiga.defineImmutableProperty @, "project", () => return @._project
|
||||||
taiga.defineImmutableProperty @, "section", () => return @._section
|
taiga.defineImmutableProperty @, "section", () => return @._section
|
||||||
taiga.defineImmutableProperty @, "sectionsBreadcrumb", () => return @._sectionsBreadcrumb
|
taiga.defineImmutableProperty @, "sectionsBreadcrumb", () => return @._sectionsBreadcrumb
|
||||||
|
taiga.defineImmutableProperty @, "activeMembers", () => return @._activeMembers
|
||||||
|
|
||||||
setSection: (section) ->
|
setSection: (section) ->
|
||||||
@._section = section
|
@._section = section
|
||||||
|
@ -22,20 +25,32 @@ class ProjectService
|
||||||
else
|
else
|
||||||
@._sectionsBreadcrumb = Immutable.List()
|
@._sectionsBreadcrumb = Immutable.List()
|
||||||
|
|
||||||
setProject: (pslug) ->
|
setProjectBySlug: (pslug) ->
|
||||||
if @._pslug != pslug
|
return new Promise (resolve, reject) =>
|
||||||
@._pslug = pslug
|
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: () ->
|
cleanProject: () ->
|
||||||
@._pslug = null
|
|
||||||
@._project = null
|
@._project = null
|
||||||
|
@._activeMembers = Immutable.List()
|
||||||
@._section = null
|
@._section = null
|
||||||
@._sectionsBreadcrumb = Immutable.List()
|
@._sectionsBreadcrumb = Immutable.List()
|
||||||
|
|
||||||
fetchProject: () ->
|
fetchProject: () ->
|
||||||
return @projectsService.getProjectBySlug(@._pslug).then (project) =>
|
pslug = @.project.get('slug')
|
||||||
@._project = project
|
|
||||||
|
return @projectsService.getProjectBySlug(pslug).then (project) => @.setProject(project)
|
||||||
|
|
||||||
angular.module("taigaCommon").service("tgProjectService", ProjectService)
|
angular.module("taigaCommon").service("tgProjectService", ProjectService)
|
||||||
|
|
|
@ -10,11 +10,19 @@ describe "tgProjectService", ->
|
||||||
|
|
||||||
$provide.value "tgProjectsService", mocks.projectsService
|
$provide.value "tgProjectsService", mocks.projectsService
|
||||||
|
|
||||||
|
_mockXhrErrorService = () ->
|
||||||
|
mocks.xhrErrorService = {
|
||||||
|
response: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
$provide.value "tgXhrErrorService", mocks.xhrErrorService
|
||||||
|
|
||||||
_mocks = () ->
|
_mocks = () ->
|
||||||
module (_$provide_) ->
|
module (_$provide_) ->
|
||||||
$provide = _$provide_
|
$provide = _$provide_
|
||||||
|
|
||||||
_mockProjectsService()
|
_mockProjectsService()
|
||||||
|
_mockXhrErrorService()
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
@ -46,31 +54,70 @@ describe "tgProjectService", ->
|
||||||
|
|
||||||
expect(projectService.sectionsBreadcrumb.toJS()).to.be.eql(breadcrumb)
|
expect(projectService.sectionsBreadcrumb.toJS()).to.be.eql(breadcrumb)
|
||||||
|
|
||||||
it "set project if the project slug has changed", () ->
|
it "set project if the project slug has changed", (done) ->
|
||||||
projectService.fetchProject = sinon.spy()
|
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.project).to.be.equal(project)
|
||||||
|
expect(projectService.activeMembers.size).to.be.equal(2)
|
||||||
expect(projectService.fetchProject).to.be.calledTwice
|
|
||||||
|
|
||||||
it "fetch project", (done) ->
|
it "fetch project", (done) ->
|
||||||
project = Immutable.Map({id: 1})
|
project = Immutable.Map({
|
||||||
pslug = "slug-1"
|
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 () ->
|
projectService.fetchProject().then () ->
|
||||||
expect(projectService.project).to.be.equal(project)
|
expect(projectService.project).to.be.equal(project)
|
||||||
done()
|
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);
|
||||||
|
|
Loading…
Reference in New Issue