Merge pull request #681 from taigaio/issue-3343-us-tasks-wiki-and-issues-links-broken-on-timeline

The problem was related to tg-repeat and the timeline item controller…
stable
Juanfran 2015-10-23 08:06:01 +02:00
commit d8955f947d
8 changed files with 143 additions and 183 deletions

View File

@ -24,7 +24,7 @@ class UserTimelineItemTitle
if user.get('is_profile_visible')
title_attr = @translate.instant('COMMON.SEE_USER_PROFILE', {username: user.get('username')})
url = "user-profile:username=vm.timeline.getIn(['data', 'user', 'username'])"
url = "user-profile:username=timeline.getIn(['data', 'user', 'username'])"
return @._getLink(url, user.get('name'), title_attr)
else
@ -36,7 +36,7 @@ class UserTimelineItemTitle
return @translate.instant(@._fieldTranslationKey[field_name])
project_name: (timeline, event) ->
url = "project:project=vm.timeline.getIn(['data', 'project', 'slug'])"
url = "project:project=timeline.getIn(['data', 'project', 'slug'])"
return @._getLink(url, timeline.getIn(["data", "project", "name"]))
@ -53,7 +53,7 @@ class UserTimelineItemTitle
return timeline.getIn(["data", "value_diff", "value"]).first().get(1)
sprint_name: (timeline, event) ->
url = "project-taskboard:project=vm.timeline.getIn(['data', 'project', 'slug']),sprint=vm.timeline.getIn(['data', 'milestone', 'slug'])"
url = "project-taskboard:project=timeline.getIn(['data', 'project', 'slug']),sprint=timeline.getIn(['data', 'milestone', 'slug'])"
return @._getLink(url, timeline.getIn(['data', 'milestone', 'name']))
@ -95,12 +95,12 @@ class UserTimelineItemTitle
_getDetailObjUrl: (event) ->
url = {
"issue": ["project-issues-detail", ":project=vm.timeline.getIn(['data', 'project', 'slug']),ref=vm.timeline.getIn(['obj', 'ref'])"],
"wikipage": ["project-wiki-page", ":project=vm.timeline.getIn(['data', 'project', 'slug']),slug=vm.timeline.getIn(['obj', 'ref'])"],
"task": ["project-tasks-detail", ":project=vm.timeline.getIn(['data', 'project', 'slug']),ref=vm.timeline.getIn(['obj', 'ref'])"],
"userstory": ["project-userstories-detail", ":project=vm.timeline.getIn(['data', 'project', 'slug']),ref=vm.timeline.getIn(['obj', 'ref'])"],
"parent_userstory": ["project-userstories-detail", ":project=vm.timeline.getIn(['data', 'project', 'slug']),ref=vm.timeline.getIn(['obj', 'userstory', 'ref'])"],
"milestone": ["project-taskboard", ":project=vm.timeline.getIn(['data', 'project', 'slug']),ref=vm.timeline.getIn(['obj', 'ref'])"]
"issue": ["project-issues-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"],
"wikipage": ["project-wiki-page", ":project=timeline.getIn(['data', 'project', 'slug']),slug=timeline.getIn(['obj', 'slug'])"],
"task": ["project-tasks-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"],
"userstory": ["project-userstories-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"],
"parent_userstory": ["project-userstories-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'userstory', 'ref'])"],
"milestone": ["project-taskboard", ":project=timeline.getIn(['data', 'project', 'slug']),sprint=timeline.getIn(['obj', 'slug'])"]
}
return url[event.obj][0] + url[event.obj][1]

View File

@ -54,7 +54,7 @@ describe "tgUserTimelineItemTitle", ->
.returns('user-param')
usernamelink = sinon.match ((value) ->
return value.username == '<a tg-nav="user-profile:username=vm.timeline.getIn([\'data\', \'user\', \'username\'])" title="user-param">oo</a>'
return value.username == '<a tg-nav="user-profile:username=timeline.getIn([\'data\', \'user\', \'username\'])" title="user-param">oo</a>'
), "usernamelink"
mockTranslate.instant
@ -173,7 +173,7 @@ describe "tgUserTimelineItemTitle", ->
}
projectparam = sinon.match ((value) ->
return value.project_name == '<a tg-nav="project:project=vm.timeline.getIn([\'data\', \'project\', \'slug\'])" title="project_name">project_name</a>'
return value.project_name == '<a tg-nav="project:project=timeline.getIn([\'data\', \'project\', \'slug\'])" title="project_name">project_name</a>'
), "projectparam"
mockTranslate.instant
@ -201,7 +201,7 @@ describe "tgUserTimelineItemTitle", ->
}
milestoneparam = sinon.match ((value) ->
return value.sprint_name == '<a tg-nav="project-taskboard:project=vm.timeline.getIn([\'data\', \'project\', \'slug\']),sprint=vm.timeline.getIn([\'data\', \'milestone\', \'slug\'])" title="milestone_name">milestone_name</a>'
return value.sprint_name == '<a tg-nav="project-taskboard:project=timeline.getIn([\'data\', \'project\', \'slug\']),sprint=timeline.getIn([\'data\', \'milestone\', \'slug\'])" title="milestone_name">milestone_name</a>'
), "milestoneparam"
mockTranslate.instant
@ -232,7 +232,7 @@ describe "tgUserTimelineItemTitle", ->
}
objparam = sinon.match ((value) ->
return value.obj_name == '<a tg-nav="project-issues-detail:project=vm.timeline.getIn([\'data\', \'project\', \'slug\']),ref=vm.timeline.getIn([\'obj\', \'ref\'])" title="#123 subject">#123 subject</a>'
return value.obj_name == '<a tg-nav="project-issues-detail:project=timeline.getIn([\'data\', \'project\', \'slug\']),ref=timeline.getIn([\'obj\', \'ref\'])" title="#123 subject">#123 subject</a>'
), "objparam"
mockTranslate.instant
@ -262,7 +262,7 @@ describe "tgUserTimelineItemTitle", ->
}
objparam = sinon.match ((value) ->
return value.obj_name == '<a tg-nav="project-wiki-page:project=vm.timeline.getIn([\'data\', \'project\', \'slug\']),slug=vm.timeline.getIn([\'obj\', \'ref\'])" title="Slug wiki">Slug wiki</a>'
return value.obj_name == '<a tg-nav="project-wiki-page:project=timeline.getIn([\'data\', \'project\', \'slug\']),slug=timeline.getIn([\'obj\', \'slug\'])" title="Slug wiki">Slug wiki</a>'
), "objparam"
mockTranslate.instant
@ -292,7 +292,7 @@ describe "tgUserTimelineItemTitle", ->
}
objparam = sinon.match ((value) ->
return value.obj_name == '<a tg-nav="project-taskboard:project=vm.timeline.getIn([\'data\', \'project\', \'slug\']),ref=vm.timeline.getIn([\'obj\', \'ref\'])" title="milestone_name">milestone_name</a>'
return value.obj_name == '<a tg-nav="project-taskboard:project=timeline.getIn([\'data\', \'project\', \'slug\']),ref=timeline.getIn([\'obj\', \'ref\'])" title="milestone_name">milestone_name</a>'
), "objparam"
mockTranslate.instant
@ -326,7 +326,7 @@ describe "tgUserTimelineItemTitle", ->
}
objparam = sinon.match ((value) ->
return value.us_name == '<a tg-nav="project-userstories-detail:project=vm.timeline.getIn([\'data\', \'project\', \'slug\']),ref=vm.timeline.getIn([\'obj\', \'userstory\', \'ref\'])" title="#2 subject">#2 subject</a>'
return value.us_name == '<a tg-nav="project-userstories-detail:project=timeline.getIn([\'data\', \'project\', \'slug\']),ref=timeline.getIn([\'obj\', \'userstory\', \'ref\'])" title="#2 subject">#2 subject</a>'
), "objparam"
mockTranslate.instant

View File

@ -1,41 +0,0 @@
class UserTimelineItemController
@.$inject = [
"tgUserTimelineItemType",
"tgUserTimelineItemTitle"
]
constructor: (@userTimelineItemType, @userTimelineItemTitle) ->
event = @.parseEventType(@.timeline.get('event_type'))
type = @userTimelineItemType.getType(@.timeline, event)
title = @userTimelineItemTitle.getTitle(@.timeline, event, type)
@.timeline = @.timeline.set('title_html', title)
@.timeline = @.timeline.set('obj', @.getObject(@.timeline, event))
if type.description
@.timeline = @.timeline.set('description', type.description(@.timeline))
if type.member
@.timeline = @.timeline.set('member', type.member(@.timeline))
if @.timeline.getIn(['data', 'value_diff', 'key']) == 'attachments' &&
@.timeline.hasIn(['data', 'value_diff', 'value', 'new'])
@.timeline = @.timeline.set('attachments', @.timeline.getIn(['data', 'value_diff', 'value', 'new']))
getObject: (timeline, event) ->
if timeline.get('data').get(event.obj)
return timeline.get('data').get(event.obj)
parseEventType: (event_type) ->
event_type = event_type.split(".")
return {
section: event_type[0],
obj: event_type[1],
type: event_type[2]
}
angular.module("taigaUserTimeline")
.controller("UserTimelineItem", UserTimelineItemController)

View File

@ -1,95 +0,0 @@
describe "UserTimelineItemController", ->
controller = scope = provide = null
timeline = event = null
mockTgUserTimelineItemType = null
mockTgUserTimelineItemTitle = null
mockType = null
_mockTgUserTimelineItemType = () ->
mockTgUserTimelineItemType = {
getType: sinon.stub()
}
mockType = {
description: sinon.stub(),
member: sinon.stub()
}
mockTgUserTimelineItemType.getType.withArgs(timeline).returns(mockType)
provide.value "tgUserTimelineItemType", mockTgUserTimelineItemType
_mockTgUserTimelineItemTitle = () ->
mockTgUserTimelineItemTitle = {
getTitle: sinon.stub()
}
mockTgUserTimelineItemTitle.getTitle.withArgs(timeline, event, mockType).returns("fakeTitle")
provide.value "tgUserTimelineItemTitle", mockTgUserTimelineItemTitle
_mocks = () ->
module ($provide) ->
provide = $provide
_mockTgUserTimelineItemType()
_mockTgUserTimelineItemTitle()
return null
_setup = () ->
event = {
section: 'issues',
obj: 'issue',
type: 'created'
}
timeline = Immutable.fromJS({
event_type: 'issues.issue.created',
data: {
user: 'user_fake',
project: 'project_fake',
milestone: 'milestone_fake',
created: new Date().getTime(),
issue: {
id: 2
},
value_diff: {
key: 'attachments',
value: {
new: "fakeAttachment"
}
}
}
})
scope = {
vm: {
timeline: timeline
}
}
beforeEach ->
module "taigaUserTimeline"
_setup()
_mocks()
inject ($controller) ->
controller = $controller
it "all activity fields filled", () ->
timeline = scope.vm.timeline
description = "fakeDescription"
member = "fakeMember"
mockType.description.returns(description)
mockType.member.returns(member)
myCtrl = controller("UserTimelineItem", {$scope: scope}, {timeline: timeline})
expect(myCtrl.timeline.get('title_html')).to.be.equal("fakeTitle")
expect(myCtrl.timeline.get('obj')).to.be.equal(myCtrl.timeline.getIn(["data", "issue"]))
expect(myCtrl.timeline.get("description")).to.be.equal(description)
expect(myCtrl.timeline.get("member")).to.be.equal(member)
expect(myCtrl.timeline.get("attachments")).to.be.equal("fakeAttachment")

View File

@ -1,8 +1,5 @@
UserTimelineItemDirective = () ->
return {
controllerAs: "vm"
controller: "UserTimelineItem"
bindToController: true
templateUrl: "user-timeline/user-timeline-item/user-timeline-item.html"
scope: {
timeline: "=tgUserTimelineItem"

View File

@ -1,29 +1,29 @@
div.activity-item
span.activity-date {{::vm.timeline.get('created') | momentFromNow}}
span.activity-date {{::timeline.get('created') | momentFromNow}}
div.activity-info(tg-user-timeline-title="vm.timeline")
div.activity-info(tg-user-timeline-title="timeline")
div.activity-info
// profile image with url
div.profile-contact-picture(ng-if="vm.timeline.getIn(['data', 'user', 'is_profile_visible'])")
a(tg-nav="user-profile:username=vm.timeline.getIn(['data', 'user', 'username'])", title="{{::vm.timeline.getIn(['data', 'user', 'name']) }}")
img(ng-src="{{::vm.timeline.getIn(['data', 'user', 'photo']) || '/images/user-noimage.png'}}", alt="{{::vm.timeline.getIn(['data', 'user', 'name'])}}")
div.profile-contact-picture(ng-if="timeline.getIn(['data', 'user', 'is_profile_visible'])")
a(tg-nav="user-profile:username=timeline.getIn(['data', 'user', 'username'])", title="{{::timeline.getIn(['data', 'user', 'name']) }}")
img(ng-src="{{::timeline.getIn(['data', 'user', 'photo']) || '/images/user-noimage.png'}}", alt="{{::timeline.getIn(['data', 'user', 'name'])}}")
// profile image without url
div.profile-contact-picture(ng-if="!vm.timeline.getIn(['data', 'user', 'is_profile_visible'])")
img(ng-src="{{::vm.timeline.getIn(['data', 'user', 'photo']) || '/images/user-noimage.png'}}", alt="{{::vm.timeline.getIn(['data', 'user', 'name'])}}")
div.profile-contact-picture(ng-if="!timeline.getIn(['data', 'user', 'is_profile_visible'])")
img(ng-src="{{::timeline.getIn(['data', 'user', 'photo']) || '/images/user-noimage.png'}}", alt="{{::timeline.getIn(['data', 'user', 'name'])}}")
p(tg-compile-html="vm.timeline.get('title_html')")
p(tg-compile-html="timeline.get('title_html')")
blockquote.activity-comment-quote(ng-if="::vm.timeline.get('description')")
| {{::vm.timeline.get('description') | limitTo:300}}
blockquote.activity-comment-quote(ng-if="::timeline.get('description')")
| {{::timeline.get('description') | limitTo:300}}
.activity-member-view(ng-if="::vm.timeline.has('member')")
a.profile-member-picture(tg-nav="user-profile:username=vm.timeline.getIn(['member', 'user', 'username'])", title="{{::vm.timeline.getIn(['member', 'user', 'name'])}}")
img(ng-src="{{::vm.timeline.getIn(['member', 'user', 'photo'])}}", alt="{{::vm.timeline.getIn(['member','user', 'name'])}}")
.activity-member-view(ng-if="::timeline.has('member')")
a.profile-member-picture(tg-nav="user-profile:username=timeline.getIn(['member', 'user', 'username'])", title="{{::timeline.getIn(['member', 'user', 'name'])}}")
img(ng-src="{{::timeline.getIn(['member', 'user', 'photo'])}}", alt="{{::timeline.getIn(['member','user', 'name'])}}")
.activity-member-info
a(tg-nav="user-profile:username=vm.timeline.getIn(['member', 'user', 'username'])", title="{{::vm.timeline.getIn(['member','user', 'name'])}}")
span {{::vm.timeline.getIn(['member','user', 'name'])}}
p {{::vm.timeline.getIn(['member','role', 'name'])}}
a(tg-nav="user-profile:username=timeline.getIn(['member', 'user', 'username'])", title="{{::timeline.getIn(['member','user', 'name'])}}")
span {{::timeline.getIn(['member','user', 'name'])}}
p {{::timeline.getIn(['member','role', 'name'])}}
div(tg-repeat="attachment in vm.timeline.get('attachments')")
div(tg-repeat="attachment in timeline.get('attachments')")
div(tg-user-timeline-attachment="attachment")

View File

@ -1,9 +1,14 @@
taiga = @.taiga
class UserTimelineService extends taiga.Service
@.$inject = ["tgResources", "tgUserTimelinePaginationSequenceService"]
@.$inject = [
"tgResources",
"tgUserTimelinePaginationSequenceService",
"tgUserTimelineItemType",
"tgUserTimelineItemTitle"
]
constructor: (@rs, @userTimelinePaginationSequenceService) ->
constructor: (@rs, @userTimelinePaginationSequenceService, @userTimelineItemType, @userTimelineItemTitle) ->
_valid_fields: [
'status',
@ -75,12 +80,46 @@ class UserTimelineService extends taiga.Service
return _.some @._invalid, (invalid) =>
return invalid.check.call(this, timeline)
# create a entry per every item in the values_diff
_splitChanges: (response) ->
_parseEventType: (event_type) ->
event_type = event_type.split(".")
return {
section: event_type[0],
obj: event_type[1],
type: event_type[2]
}
_getTimelineObject: (timeline, event) ->
if timeline.get('data').get(event.obj)
return timeline.get('data').get(event.obj)
_attachExtraInfoToTimelineEntry: (timeline, event, type) ->
title = @userTimelineItemTitle.getTitle(timeline, event, type)
timeline = timeline.set('title_html', title)
timeline = timeline.set('obj', @._getTimelineObject(timeline, event))
if type.description
timeline = timeline.set('description', type.description(timeline))
if type.member
timeline = timeline.set('member', type.member(timeline))
if timeline.getIn(['data', 'value_diff', 'key']) == 'attachments' &&
timeline.hasIn(['data', 'value_diff', 'value', 'new'])
timeline = timeline.set('attachments', timeline.getIn(['data', 'value_diff', 'value', 'new']))
return timeline
# - create a entry per every item in the values_diff
# - add extra attributes to each entry
_parseTimeline: (response) ->
newdata = Immutable.List()
response.get('data').forEach (item) ->
event_type = item.get('event_type').split(".")
response.get('data').forEach (item) =>
event = @._parseEventType(item.get('event_type'))
type = @userTimelineItemType.getType(item, event)
data = item.get('data')
values_diff = data.get('values_diff')
@ -92,10 +131,10 @@ class UserTimelineService extends taiga.Service
if values_diff.has('milestone')
values_diff = Immutable.Map({'moveInBacklog': values_diff})
else if event_type[1] == 'milestone'
else if event.obj == 'milestone'
values_diff = Immutable.Map({'milestone': values_diff})
values_diff.forEach (value, key) ->
values_diff.forEach (value, key) =>
obj = Immutable.Map({
key: key,
value: value
@ -103,9 +142,11 @@ class UserTimelineService extends taiga.Service
newItem = item.setIn(['data', 'value_diff'], obj)
newItem = newItem.deleteIn(['data', 'values_diff'])
newItem = @._attachExtraInfoToTimelineEntry(newItem, event, type)
newdata = newdata.push(newItem)
else
newItem = item.deleteIn(['data', 'values_diff'])
newItem = @._attachExtraInfoToTimelineEntry(newItem, event, type)
newdata = newdata.push(newItem)
return response.set('data', newdata)
@ -116,7 +157,7 @@ class UserTimelineService extends taiga.Service
config.fetch = (page) =>
return @rs.users.getProfileTimeline(userId, page)
.then (response) =>
return @._splitChanges(response)
return @._parseTimeline(response)
config.filter = (items) =>
return items.filterNot (item) => @._isInValidTimeline(item)
@ -129,7 +170,7 @@ class UserTimelineService extends taiga.Service
config.fetch = (page) =>
return @rs.users.getUserTimeline(userId, page)
.then (response) =>
return @._splitChanges(response)
return @._parseTimeline(response)
config.filter = (items) =>
return items.filterNot (item) => @._isInValidTimeline(item)
@ -141,7 +182,7 @@ class UserTimelineService extends taiga.Service
config.fetch = (page) =>
return @rs.projects.getTimeline(projectId, page)
.then (response) => return @._splitChanges(response)
.then (response) => return @._parseTimeline(response)
config.filter = (items) =>
return items.filterNot (item) => @._isInValidTimeline(item)

View File

@ -23,11 +23,34 @@ describe "tgUserTimelineService", ->
provide.value "tgUserTimelinePaginationSequenceService", mocks.userTimelinePaginationSequence
_mockTgUserTimelineItemType = () ->
mocks.userTimelineItemType = {
getType: sinon.stub()
}
mocks.getType = {
description: sinon.stub(),
member: sinon.stub()
}
mocks.userTimelineItemType.getType.returns(mocks.getType)
provide.value "tgUserTimelineItemType", mocks.userTimelineItemType
_mockTgUserTimelineItemTitle = () ->
mocks.userTimelineItemTitle = {
getTitle: sinon.stub()
}
provide.value "tgUserTimelineItemTitle", mocks.userTimelineItemTitle
_mocks = () ->
module ($provide) ->
provide = $provide
_mockResources()
_mockUserTimelinePaginationSequence()
_mockTgUserTimelineItemType()
_mockTgUserTimelineItemTitle()
return null
@ -211,3 +234,38 @@ describe "tgUserTimelineService", ->
result = userTimelineService.getProjectTimeline(userId)
expect(result).to.be.eventually.true
it "all timeline extra fields filled", () ->
timeline = Immutable.fromJS({
data: [{
event_type: 'issues.issue.created',
data: {
user: 'user_fake',
project: 'project_fake',
milestone: 'milestone_fake',
created: new Date().getTime(),
issue: {
id: 2
},
value_diff: {
key: 'attachments',
value: {
new: "fakeAttachment"
}
}
}
}]
})
mocks.userTimelineItemTitle.getTitle.returns("fakeTitle")
mocks.getType.description.returns("fakeDescription")
mocks.getType.member.returns("fakeMember")
timeline = userTimelineService._parseTimeline(timeline)
timelineEntry = timeline.get('data').get(0)
expect(timelineEntry.get('title_html')).to.be.equal("fakeTitle")
expect(timelineEntry.get('obj')).to.be.equal(timelineEntry.getIn(["data", "issue"]))
expect(timelineEntry.get("description")).to.be.equal("fakeDescription")
expect(timelineEntry.get("member")).to.be.equal("fakeMember")
expect(timelineEntry.get("attachments")).to.be.equal("fakeAttachment")