timeline with immutable objects

stable
Juanfran 2015-05-06 10:55:32 +02:00
parent c2c40ac7a4
commit 2eef2a35f4
13 changed files with 238 additions and 118 deletions

View File

@ -1,3 +0,0 @@
section.profile-timeline(ng-controller="ProfileTimeline as ctrl")
div(infinite-scroll="ctrl.loadTimeline()", infinite-scroll-distance="3", infinite-scroll-disabled='ctrl.loadingData')
div(ng-repeat="timeline in ctrl.timelineList", tg-profile-timeline-item="timeline")

View File

@ -21,7 +21,7 @@ class ProfileTimelineItemTitle
if param == "username" if param == "username"
user = timeline.data.user user = timeline.data.user
title_attr = @translate.instant('COMMON.SEE_USER_PROFILE', {username: user.username}) title_attr = @translate.instant('COMMON.SEE_USER_PROFILE', {username: user.username})
url = 'user-profile:username=activity.user.username' url = 'user-profile:username=vm.activity.user.username'
return @._getLink(url, user.username, title_attr) return @._getLink(url, user.username, title_attr)
else if param == 'field_name' else if param == 'field_name'
@ -30,12 +30,12 @@ class ProfileTimelineItemTitle
return @translate.instant(@._fieldTranslationKey[field_name]) return @translate.instant(@._fieldTranslationKey[field_name])
else if param == 'project_name' else if param == 'project_name'
url = 'project:project=activity.project.slug' url = 'project:project=vm.activity.project.slug'
return @._getLink(url, timeline.data.project.name) return @._getLink(url, timeline.data.project.name)
else if param == 'sprint_name' else if param == 'sprint_name'
url = 'project-taskboard:project=activity.project.slug,sprint=activity.sprint.slug' url = 'project-taskboard:project=vm.activity.project.slug,sprint=vm.activity.sprint.slug'
return @._getLink(url, timeline.data.milestone.name) return @._getLink(url, timeline.data.milestone.name)

View File

@ -52,7 +52,7 @@ describe "tgProfileTimelineItemTitle", ->
.returns('user-param') .returns('user-param')
usernamelink = sinon.match ((value) -> usernamelink = sinon.match ((value) ->
return value.username == '<a tg-nav="user-profile:username=activity.user.username" title="user-param">xx</a>' return value.username == '<a tg-nav="user-profile:username=vm.activity.user.username" title="user-param">xx</a>'
), "usernamelink" ), "usernamelink"
mockTranslate.instant mockTranslate.instant
@ -113,7 +113,7 @@ describe "tgProfileTimelineItemTitle", ->
} }
projectparam = sinon.match ((value) -> projectparam = sinon.match ((value) ->
return value.project_name == '<a tg-nav="project:project=activity.project.slug" title="project_name">project_name</a>' return value.project_name == '<a tg-nav="project:project=vm.activity.project.slug" title="project_name">project_name</a>'
), "projectparam" ), "projectparam"
mockTranslate.instant mockTranslate.instant
@ -141,7 +141,7 @@ describe "tgProfileTimelineItemTitle", ->
} }
milestoneparam = sinon.match ((value) -> milestoneparam = sinon.match ((value) ->
return value.sprint_name == '<a tg-nav="project-taskboard:project=activity.project.slug,sprint=activity.sprint.slug" title="milestone_name">milestone_name</a>' return value.sprint_name == '<a tg-nav="project-taskboard:project=vm.activity.project.slug,sprint=vm.activity.sprint.slug" title="milestone_name">milestone_name</a>'
), "milestoneparam" ), "milestoneparam"
mockTranslate.instant mockTranslate.instant

View File

@ -1,31 +1,33 @@
class ProfileTimelineItemController class ProfileTimelineItemController
@.$inject = [ @.$inject = [
"$scope",
"$sce", "$sce",
"tgProfileTimelineItemType", "tgProfileTimelineItemType",
"tgProfileTimelineItemTitle" "tgProfileTimelineItemTitle"
] ]
constructor: (@scope, @sce, @profileTimelineItemType, @profileTimelineItemTitle) -> constructor: (@sce, @profileTimelineItemType, @profileTimelineItemTitle) ->
event = @parseEventType(@scope.vm.timeline.event_type) timeline = @.timeline
type = @profileTimelineItemType.getType(@scope.vm.timeline, event) event = @.parseEventType(timeline.event_type)
type = @profileTimelineItemType.getType(timeline, event)
@.activity = {} @.activity = {}
@.activity.user = @scope.vm.timeline.data.user @.activity.user = timeline.data.user
@.activity.project = @scope.vm.timeline.data.project @.activity.project = timeline.data.project
@.activity.sprint = @scope.vm.timeline.data.milestone @.activity.sprint = timeline.data.milestone
@.activity.title = @profileTimelineItemTitle.getTitle(@scope.vm.timeline, event, type) @.activity.title = @profileTimelineItemTitle.getTitle(timeline, event, type)
@.activity.created_formated = moment(@scope.vm.timeline.created).fromNow() @.activity.created_formated = moment(timeline.created).fromNow()
#test
@.activity.obj = @.getObject(timeline, event)
if type.description if type.description
@.activity.description = @sce.trustAsHtml(type.description(@scope.vm.timeline)) @.activity.description = @sce.trustAsHtml(type.description(timeline))
if type.member if type.member
@.activity.member = type.member(@scope.vm.timeline) @.activity.member = type.member(timeline)
if @scope.vm.timeline.data.values_diff?.attachments if timeline.data.values_diff?.attachments
@.activity.attachments = @scope.vm.timeline.data.values_diff.attachments.new @.activity.attachments = timeline.data.values_diff.attachments.new
parseEventType: (event_type) -> parseEventType: (event_type) ->
event_type = event_type.split(".") event_type = event_type.split(".")
@ -36,5 +38,9 @@ class ProfileTimelineItemController
type: event_type[2] type: event_type[2]
} }
getObject: (timeline, event) ->
if timeline.data[event.obj]
return timeline.data[event.obj]
angular.module("taigaProfile") angular.module("taigaProfile")
.controller("ProfileTimelineItem", ProfileTimelineItemController) .controller("ProfileTimelineItem", ProfileTimelineItemController)

View File

@ -72,7 +72,7 @@ describe "ProfileTimelineItemController", ->
it "basic activity fields filled", () -> it "basic activity fields filled", () ->
timeline = scope.vm.timeline timeline = scope.vm.timeline
myCtrl = controller("ProfileTimelineItem", {$scope: scope}) myCtrl = controller("ProfileTimelineItem", {$scope: scope}, {timeline: timeline})
expect(myCtrl.activity.user).to.be.equal(timeline.data.user) expect(myCtrl.activity.user).to.be.equal(timeline.data.user)
expect(myCtrl.activity.project).to.be.equal(timeline.data.project) expect(myCtrl.activity.project).to.be.equal(timeline.data.project)
@ -94,7 +94,7 @@ describe "ProfileTimelineItemController", ->
mockType.description.withArgs(timeline).returns(description) mockType.description.withArgs(timeline).returns(description)
mockType.member.withArgs(timeline).returns(member) mockType.member.withArgs(timeline).returns(member)
myCtrl = controller("ProfileTimelineItem", {$scope: scope}) myCtrl = controller("ProfileTimelineItem", {$scope: scope}, {timeline: timeline})
expect(myCtrl.activity.description).to.be.an('object') # $sce.trustAsHtml expect(myCtrl.activity.description).to.be.an('object') # $sce.trustAsHtml
expect(myCtrl.activity.member).to.be.equal(member) expect(myCtrl.activity.member).to.be.equal(member)

View File

@ -14,7 +14,7 @@ div.activity-image
.profile-member-picture .profile-member-picture
img(ng-src="{{::vm.activity.member.user.photo}}", alt="{{::vm.activity.member.user.name}}") img(ng-src="{{::vm.activity.member.user.photo}}", alt="{{::vm.activity.member.user.name}}")
.activity-member-info .activity-member-info
a(tg-nav="user-profile:username=activity.member.user.username", title="{{::vm.activity.member.user.name }}") a(tg-nav="user-profile:username=vm.activity.member.user.username", title="{{::vm.activity.member.user.name }}")
span {{::vm.activity.member.user.name}} span {{::vm.activity.member.user.name}}
p {{::vm.activity.member.role.name}} p {{::vm.activity.member.role.name}}

View File

@ -25,58 +25,25 @@ mixOf = @.taiga.mixOf
class ProfileTimelineController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) class ProfileTimelineController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
@.$inject = [ @.$inject = [
"$tgResources", "$tgAuth",
"$tgAuth" "tgProfileTimelineService"
] ]
_valid_fields: [ constructor: (@auth, @profileTimelineService) ->
'status', @.timelineList = Immutable.List()
'subject', @.page = 1
'description',
'assigned_to',
'points',
'severity',
'priority',
'type',
'attachments',
'milestone',
'is_blocked',
'is_iocaine',
'content_diff',
'name',
'estimated_finish',
'estimated_start'
]
constructor: (@rs, @auth) ->
@.timelineList = []
@.pagination = {page: 1}
@.loadingData = false @.loadingData = false
_isValidField: (values) =>
return _.some values, (value) => @._valid_fields.indexOf(value) != -1
_filterValidTimelineItems: (timeline) =>
if timeline.data.values_diff
values = Object.keys(timeline.data.values_diff)
if values && values.length
if !@._isValidField(values)
return false
else if values[0] == 'attachments' && timeline.data.values_diff.attachments.new.length == 0
return false
return true
loadTimeline: () -> loadTimeline: () ->
user = @auth.getUser() user = @auth.getUser()
@.loadingData = true @.loadingData = true
return @rs.timeline.profile(user.id, @.pagination).then (result) => @profileTimelineService
newTimelineList = _.filter result.data, @._filterValidTimelineItems .getTimeline(user.id, @.page)
@.timelineList = @timelineList.concat(newTimelineList) .then (newTimelineList) =>
@.pagination.page++ @.timelineList = @.timelineList.concat(newTimelineList)
@.page++
@.loadingData = false @.loadingData = false
angular.module("taigaProfile") angular.module("taigaProfile")

View File

@ -1,13 +1,17 @@
describe "ProfileTimelineController", -> describe "ProfileTimelineController", ->
myCtrl = scope = $q = provide = null myCtrl = scope = $q = provide = null
mocks = {}
mockUser = {id: 3} mockUser = {id: 3}
_mockTgResources = () -> _mockProfileTimeline = () ->
provide.value "$tgResources", { mocks.profileTimelineService = {
timeline: {} getTimeline: sinon.stub()
} }
provide.value "tgProfileTimelineService", mocks.profileTimelineService
_mockTgAuth = () -> _mockTgAuth = () ->
provide.value "$tgAuth", { provide.value "$tgAuth", {
getUser: () -> getUser: () ->
@ -17,7 +21,7 @@ describe "ProfileTimelineController", ->
_mocks = () -> _mocks = () ->
module ($provide) -> module ($provide) ->
provide = $provide provide = $provide
_mockTgResources() _mockProfileTimeline()
_mockTgAuth() _mockTgAuth()
return null return null
@ -32,60 +36,31 @@ describe "ProfileTimelineController", ->
myCtrl = $controller "ProfileTimeline" myCtrl = $controller "ProfileTimeline"
it "timelineList should be an array", () -> it "timelineList should be an array", () ->
expect(myCtrl.timelineList).is.an("array") expect(myCtrl.timelineList.toJS()).is.an("array")
it "pagination starts at 1", () -> it "pagination starts at 1", () ->
expect(myCtrl.pagination.page).to.be.equal(1) expect(myCtrl.page).to.be.equal(1)
describe "load timeline", () -> describe "load timeline", () ->
thenStub = timelineList = null thenStub = timelineList = null
beforeEach () -> beforeEach () ->
timelineList = { timelineList = Immutable.fromJS([
data: [ { fake: "fake"},
{ # valid item { fake: "fake"},
data: { { fake: "fake"},
values_diff: { { fake: "fake"}
"status": "xx", ])
"subject": "xx"
}
}
},
{ # invalid item
data: {
values_diff: {
"fake": "xx"
}
}
},
{ # invalid item
data: {
values_diff: {
"fake2": "xx"
}
}
},
{ # valid item
data: {
values_diff: {
"fake2": "xx",
"milestone": "xx"
}
}
}
]
}
thenStub = sinon.stub() thenStub = sinon.stub()
profileStub = sinon.stub() profileStub = sinon.stub()
.withArgs(mockUser.id, myCtrl.pagination) .withArgs(mockUser.id, myCtrl.page)
.returns({ .returns({
then: thenStub then: thenStub
}) })
myCtrl.rs.timeline.profile = profileStub mocks.profileTimelineService.getTimeline = profileStub
it "the loadingData variable must be true during the timeline load", () -> it "the loadingData variable must be true during the timeline load", () ->
expect(myCtrl.loadingData).to.be.false expect(myCtrl.loadingData).to.be.false
@ -99,18 +74,17 @@ describe "ProfileTimelineController", ->
expect(myCtrl.loadingData).to.be.false expect(myCtrl.loadingData).to.be.false
it "pagiantion increase one every call to loadTimeline", () -> it "pagiantion increase one every call to loadTimeline", () ->
expect(myCtrl.pagination.page).to.equal(1) expect(myCtrl.page).to.equal(1)
myCtrl.loadTimeline() myCtrl.loadTimeline()
thenStub.callArgWith(0, timelineList) thenStub.callArgWith(0, timelineList)
expect(myCtrl.pagination.page).to.equal(2) expect(myCtrl.page).to.equal(2)
it "filter the invalid timeline items", () -> it "timeline items", () ->
myCtrl.loadTimeline() myCtrl.loadTimeline()
thenStub.callArgWith(0, timelineList) thenStub.callArgWith(0, timelineList)
expect(myCtrl.timelineList[0]).to.be.equal(timelineList.data[0]) expect(myCtrl.timelineList.size).to.be.eql(4)
expect(myCtrl.timelineList[1]).to.be.equal(timelineList.data[3])

View File

@ -0,0 +1,9 @@
ProfileTimelineDirective = ->
return {
templateUrl: "profile/profile-timeline/profile-timeline.html",
controller: "ProfileTimeline",
controllerAs: "vm",
scope: {}
}
angular.module("taigaProfile").directive("tgProfileTimeline", ProfileTimelineDirective)

View File

@ -0,0 +1,3 @@
section.profile-timeline
div(infinite-scroll="vm.loadTimeline()", infinite-scroll-distance="3", infinite-scroll-disabled='vm.loadingData')
div(tg-repeat="timeline in vm.timelineList", tg-profile-timeline-item="timeline")

View File

@ -0,0 +1,51 @@
taiga = @.taiga
class ProfileTimelineService extends taiga.Service
@.$inject = ["$tgResources"]
constructor: (@rs) ->
_valid_fields: [
'status',
'subject',
'description',
'assigned_to',
'points',
'severity',
'priority',
'type',
'attachments',
'milestone',
'is_blocked',
'is_iocaine',
'content_diff',
'name',
'estimated_finish',
'estimated_start'
]
_isValidField: (values) =>
return _.some values, (value) => @._valid_fields.indexOf(value) != -1
_filterValidTimelineItems: (timeline) =>
if timeline.data.values_diff
values = Object.keys(timeline.data.values_diff)
if values && values.length
if !@._isValidField(values)
return false
else if values[0] == 'attachments' &&
timeline.data.values_diff.attachments.new.length == 0
return false
return true
getTimeline: (userId, page) ->
return @rs.timeline.profile(userId, page)
.then (result) =>
newTimelineList = _.filter result.data, @._filterValidTimelineItems
return Immutable.fromJS(newTimelineList)
angular.module("taigaProjects").service("tgProfileTimelineService", ProfileTimelineService)

View File

@ -0,0 +1,113 @@
describe "tgProfileTimelineService", ->
provide = null
$q = null
$rootScope = null
profileTimelineService = null
mocks = {}
_mockResources = () ->
mocks.resources = {}
mocks.resources.timeline = {
profile: sinon.stub()
}
provide.value "$tgResources", mocks.resources
_mocks = () ->
module ($provide) ->
provide = $provide
_mockResources()
return null
_setup = ->
_mocks()
_inject = (callback) ->
inject (_tgProfileTimelineService_, _$q_, _$rootScope_) ->
profileTimelineService = _tgProfileTimelineService_
$q = _$q_
$rootScope = _$rootScope_
callback() if callback
beforeEach ->
module "taigaProjects"
_setup()
_inject()
it "filter invalid timeline items", (done) ->
valid_items = {
data: [
{ # valid item
data: {
values_diff: {
"status": "xx",
"subject": "xx"
}
}
},
{ # invalid item
data: {
values_diff: {
"fake": "xx"
}
}
},
{ # invalid item
data: {
values_diff: {
"fake2": "xx"
}
}
},
{ # valid item
data: {
values_diff: {
"fake2": "xx",
"milestone": "xx"
}
}
},
{ # invalid item
data: {
values_diff: {
attachments: {
new: []
}
}
}
},
{ # valid item
data: {
values_diff: {
attachments: {
new: [1, 2]
}
}
}
}
]
}
userId = 3
page = 2
mocks.resources.timeline.profile = (_userId_, _page_) ->
expect(_userId_).to.be.equal(userId)
expect(_page_).to.be.equal(page)
return $q (resolve, reject) ->
resolve(valid_items)
profileTimelineService.getTimeline(userId, page)
.then (_items_) ->
items = _items_.toJS()
expect(items[0]).to.be.eql(valid_items.data[0])
expect(items[1]).to.be.eql(valid_items.data[3])
expect(items[2]).to.be.eql(valid_items.data[5])
done()
$rootScope.$apply()

View File

@ -7,7 +7,7 @@ div.profile.centered
div.content-wrapper div.content-wrapper
div.content div.content
div(tg-profile-tab="activity", tab-title="Activity Tab", tab-icon="icon-timeline", tab-active) div(tg-profile-tab="activity", tab-title="Activity Tab", tab-icon="icon-timeline", tab-active)
include includes/profile-timeline div(tg-profile-timeline)
div(tg-profile-tab="projects", tab-title="Projects Tab", tab-icon="icon-project") div(tg-profile-tab="projects", tab-title="Projects Tab", tab-icon="icon-project")
div(tg-profile-projects) div(tg-profile-projects)