From a7479365bc7b6a2319869db77128530462b228c2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 22 Oct 2015 10:41:14 +0200 Subject: [PATCH 1/2] The problem was related to tg-repeat and the timeline item controller adding attributes on the constructor. The logic from this constructor has been moved to the user timeline service so that when tg-repeat compare the new nodes the new attributes are properly calculated. --- .../user-timeline-item-title.service.coffee | 18 ++-- ...er-timeline-item-title.service.spec.coffee | 14 +-- .../user-timeline-item.controller.coffee | 41 -------- .../user-timeline-item.controller.spec.coffee | 95 ------------------- .../user-timeline-item.directive.coffee | 3 - .../user-timeline-item.jade | 34 +++---- .../user-timeline.service.coffee | 63 +++++++++--- .../user-timeline.service.spec.coffee | 58 +++++++++++ 8 files changed, 143 insertions(+), 183 deletions(-) delete mode 100644 app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.coffee delete mode 100644 app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.spec.coffee diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee index c39e2b0a..dceb4247 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee @@ -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']),ref=timeline.getIn(['obj', 'ref'])"] } return url[event.obj][0] + url[event.obj][1] diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee index 4f09c32e..39f35dd2 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee @@ -54,7 +54,7 @@ describe "tgUserTimelineItemTitle", -> .returns('user-param') usernamelink = sinon.match ((value) -> - return value.username == 'oo' + return value.username == 'oo' ), "usernamelink" mockTranslate.instant @@ -173,7 +173,7 @@ describe "tgUserTimelineItemTitle", -> } projectparam = sinon.match ((value) -> - return value.project_name == 'project_name' + return value.project_name == 'project_name' ), "projectparam" mockTranslate.instant @@ -201,7 +201,7 @@ describe "tgUserTimelineItemTitle", -> } milestoneparam = sinon.match ((value) -> - return value.sprint_name == 'milestone_name' + return value.sprint_name == 'milestone_name' ), "milestoneparam" mockTranslate.instant @@ -232,7 +232,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.obj_name == '#123 subject' + return value.obj_name == '#123 subject' ), "objparam" mockTranslate.instant @@ -262,7 +262,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.obj_name == 'Slug wiki' + return value.obj_name == 'Slug wiki' ), "objparam" mockTranslate.instant @@ -292,7 +292,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.obj_name == 'milestone_name' + return value.obj_name == 'milestone_name' ), "objparam" mockTranslate.instant @@ -326,7 +326,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.us_name == '#2 subject' + return value.us_name == '#2 subject' ), "objparam" mockTranslate.instant diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.coffee deleted file mode 100644 index f73b1545..00000000 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.coffee +++ /dev/null @@ -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) diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.spec.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.spec.coffee deleted file mode 100644 index bbad8f5b..00000000 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.spec.coffee +++ /dev/null @@ -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") diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee index 15daf833..89881de3 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee @@ -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" diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade b/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade index f549c8a2..e37c1dca 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade @@ -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") diff --git a/app/modules/user-timeline/user-timeline/user-timeline.service.coffee b/app/modules/user-timeline/user-timeline/user-timeline.service.coffee index 5ba77b7b..6f4b4c52 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.service.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.service.coffee @@ -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) diff --git a/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee b/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee index 11aae951..80b41000 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee @@ -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") From d78ed823fac30aa995d9dba8756b627276875170 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Fri, 23 Oct 2015 08:04:51 +0200 Subject: [PATCH 2/2] fix timeline taskboard url --- .../user-timeline-item/user-timeline-item-title.service.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee index dceb4247..913c2e4b 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee @@ -100,7 +100,7 @@ class UserTimelineItemTitle "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']),ref=timeline.getIn(['obj', 'ref'])"] + "milestone": ["project-taskboard", ":project=timeline.getIn(['data', 'project', 'slug']),sprint=timeline.getIn(['obj', 'slug'])"] } return url[event.obj][0] + url[event.obj][1]