houston-roadmaps 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +358 -0
- data/MIT-LICENSE +20 -0
- data/README.md +31 -0
- data/Rakefile +25 -0
- data/app/assets/images/houston/roadmaps/.gitkeep +0 -0
- data/app/assets/javascripts/houston/roadmaps/app.coffee +2 -0
- data/app/assets/javascripts/houston/roadmaps/application.js +14 -0
- data/app/assets/javascripts/houston/roadmaps/handlebars_helpers.coffee +22 -0
- data/app/assets/javascripts/houston/roadmaps/jquery_extensions.coffee +4 -0
- data/app/assets/javascripts/houston/roadmaps/models/goal.coffee +11 -0
- data/app/assets/javascripts/houston/roadmaps/models/milestone.coffee +124 -0
- data/app/assets/javascripts/houston/roadmaps/models/roadmap.coffee +16 -0
- data/app/assets/javascripts/houston/roadmaps/models/viewport.coffee +3 -0
- data/app/assets/javascripts/houston/roadmaps/resize_listener.js +67 -0
- data/app/assets/javascripts/houston/roadmaps/views/_gantt_chart.coffee +224 -0
- data/app/assets/javascripts/houston/roadmaps/views/_show_milestone_view.coffee +204 -0
- data/app/assets/javascripts/houston/roadmaps/views/edit_gantt_chart.coffee +405 -0
- data/app/assets/javascripts/houston/roadmaps/views/edit_milestone_view.coffee +97 -0
- data/app/assets/javascripts/houston/roadmaps/views/edit_roadmap_view.coffee +80 -0
- data/app/assets/javascripts/houston/roadmaps/views/gantt_thumbnail_view.coffee +173 -0
- data/app/assets/javascripts/houston/roadmaps/views/goal_view.coffee +18 -0
- data/app/assets/javascripts/houston/roadmaps/views/project_goals_view.coffee +30 -0
- data/app/assets/javascripts/houston/roadmaps/views/roadmap_history_view.coffee +67 -0
- data/app/assets/javascripts/houston/roadmaps/views/roadmap_view.coffee +12 -0
- data/app/assets/javascripts/houston/roadmaps/views/roadmaps_view.coffee +29 -0
- data/app/assets/stylesheets/houston/roadmaps/application.css +13 -0
- data/app/assets/stylesheets/houston/roadmaps/colors.scss.erb +41 -0
- data/app/assets/stylesheets/houston/roadmaps/goals.scss +20 -0
- data/app/assets/stylesheets/houston/roadmaps/milestone.scss +58 -0
- data/app/assets/stylesheets/houston/roadmaps/roadmap.scss +276 -0
- data/app/assets/stylesheets/houston/roadmaps/roadmap_history.scss +42 -0
- data/app/assets/stylesheets/houston/roadmaps/roadmaps.scss +89 -0
- data/app/assets/templates/houston/roadmaps/goals/edit.hbs +6 -0
- data/app/assets/templates/houston/roadmaps/goals/index.hbs +9 -0
- data/app/assets/templates/houston/roadmaps/goals/new.hbs +27 -0
- data/app/assets/templates/houston/roadmaps/goals/show.hbs +6 -0
- data/app/assets/templates/houston/roadmaps/milestone/edit.hbs +67 -0
- data/app/assets/templates/houston/roadmaps/milestone/show.hbs +53 -0
- data/app/assets/templates/houston/roadmaps/milestone/ticket.hbs +28 -0
- data/app/assets/templates/houston/roadmaps/roadmap/goal.hbs +6 -0
- data/app/assets/templates/houston/roadmaps/roadmap/history.hbs +22 -0
- data/app/assets/templates/houston/roadmaps/roadmap/show.hbs +12 -0
- data/app/assets/templates/houston/roadmaps/roadmaps/index.hbs +2 -0
- data/app/assets/templates/houston/roadmaps/roadmaps/show.hbs +11 -0
- data/app/controllers/houston/roadmaps/api/v1/roadmap_controller.rb +20 -0
- data/app/controllers/houston/roadmaps/dashboard_controller.rb +34 -0
- data/app/controllers/houston/roadmaps/milestones_controller.rb +118 -0
- data/app/controllers/houston/roadmaps/project_goals_controller.rb +24 -0
- data/app/controllers/houston/roadmaps/roadmap_milestones_controller.rb +36 -0
- data/app/controllers/houston/roadmaps/roadmaps_controller.rb +77 -0
- data/app/helpers/houston/roadmaps/application_helper.rb +4 -0
- data/app/models/roadmap.rb +9 -0
- data/app/models/roadmap_commit.rb +35 -0
- data/app/models/roadmap_milestone.rb +29 -0
- data/app/models/roadmap_milestone_version.rb +8 -0
- data/app/presenters/houston/roadmaps/milestone_api_presenter.rb +27 -0
- data/app/presenters/houston/roadmaps/milestone_presenter.rb +74 -0
- data/app/presenters/houston/roadmaps/roadmap_milestone_presenter.rb +70 -0
- data/app/presenters/houston/roadmaps/roadmap_presenter.rb +37 -0
- data/app/presenters/houston/roadmaps/ticket_presenter.rb +20 -0
- data/app/views/houston/roadmaps/dashboard/show.html.erb +42 -0
- data/app/views/houston/roadmaps/milestones/show.html.erb +40 -0
- data/app/views/houston/roadmaps/project_goals/index.html.erb +42 -0
- data/app/views/houston/roadmaps/roadmaps/_form.html.erb +29 -0
- data/app/views/houston/roadmaps/roadmaps/edit.html.erb +8 -0
- data/app/views/houston/roadmaps/roadmaps/history.html.erb +41 -0
- data/app/views/houston/roadmaps/roadmaps/index.html.erb +24 -0
- data/app/views/houston/roadmaps/roadmaps/new.html.erb +7 -0
- data/app/views/houston/roadmaps/roadmaps/show.html.erb +27 -0
- data/app/views/houston/roadmaps/roadmaps/show_editable.html.erb +35 -0
- data/app/views/layouts/houston/roadmaps/application.html.erb +10 -0
- data/app/views/layouts/houston/roadmaps/dashboard.html.erb +9 -0
- data/bin/rails +8 -0
- data/config/database.yml +13 -0
- data/config/initializers/add_navigation_renderer.rb +13 -0
- data/config/routes.rb +37 -0
- data/db/.keep +0 -0
- data/db/migrate/20140831210254_add_band_to_milestones.rb +5 -0
- data/db/migrate/20140907212311_add_end_date_to_milestones.rb +19 -0
- data/db/migrate/20140916230539_add_locked_to_milestone.rb +5 -0
- data/db/migrate/20140927154728_add_closed_ticket_count_to_milestones.rb +11 -0
- data/db/migrate/20140929024130_create_roadmap_commits.rb +29 -0
- data/db/migrate/20141012023628_add_user_id_to_milestone_versions.rb +11 -0
- data/db/migrate/20150102192805_add_lanes_to_milestones.rb +5 -0
- data/db/migrate/20150119155145_add_goal_and_feedback_query_to_milestones.rb +6 -0
- data/db/migrate/20150524203903_add_project_id_to_roadmap_commits.rb +17 -0
- data/db/migrate/20150603203744_add_timestamps_to_roadmap_commits.rb +20 -0
- data/db/migrate/20160206214746_rename_roadmap_feature_to_milestones.rb +25 -0
- data/db/migrate/20160207154530_create_roadmaps.rb +125 -0
- data/db/structure.sql +2557 -0
- data/houston-roadmaps.gemspec +29 -0
- data/lib/houston-roadmaps.rb +1 -0
- data/lib/houston/roadmaps.rb +15 -0
- data/lib/houston/roadmaps/configuration.rb +14 -0
- data/lib/houston/roadmaps/engine.rb +27 -0
- data/lib/houston/roadmaps/milestone_ext.rb +14 -0
- data/lib/houston/roadmaps/project_ext.rb +20 -0
- data/lib/houston/roadmaps/railtie.rb +17 -0
- data/lib/houston/roadmaps/version.rb +5 -0
- data/lib/tasks/roadmap_tasks.rake +4 -0
- data/test/acceptance/houston_dummy_test.rb +17 -0
- data/test/dummy/houston.rb +23 -0
- data/test/fixtures/projects.yml +3 -0
- data/test/fixtures/users.yml +10 -0
- data/test/test_helper.rb +43 -0
- data/test/unit/fixtures_test.rb +11 -0
- metadata +227 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
class Roadmaps.ShowMilestoneView extends @TicketsView
|
2
|
+
supportsSorting: false
|
3
|
+
template: HandlebarsTemplates['houston/roadmaps/milestone/show']
|
4
|
+
|
5
|
+
initialize: ->
|
6
|
+
@milestone = @options.milestone
|
7
|
+
@id = @milestone.id
|
8
|
+
@projectTicketTracker = @options.projectTicketTracker
|
9
|
+
@usesFeedback = @options.usesFeedback
|
10
|
+
super
|
11
|
+
|
12
|
+
@$el.on 'click', '#show_completed_tickets', _.bind(@toggleShowCompleted, @)
|
13
|
+
|
14
|
+
render: ->
|
15
|
+
tickets = for ticket in @tickets.models
|
16
|
+
json = ticket.toJSON()
|
17
|
+
json.estimatedEffortCompleted = ticket.estimatedEffortCompleted()
|
18
|
+
json.estimatedEffort = ticket.estimatedEffort()
|
19
|
+
json
|
20
|
+
|
21
|
+
html = @template
|
22
|
+
projectSlug: @project
|
23
|
+
milestone: @milestone
|
24
|
+
usesFeedback: @usesFeedback
|
25
|
+
tickets: tickets
|
26
|
+
@$el.html html
|
27
|
+
|
28
|
+
complete = _.select(tickets, (ticket)-> !!ticket.closedAt).length / tickets.length
|
29
|
+
if _.isNaN(complete)
|
30
|
+
$('.milestone-progress').hide()
|
31
|
+
else
|
32
|
+
$('#milestone_progress').html(App.formatPercent complete)
|
33
|
+
$('.milestone-progress').show()
|
34
|
+
|
35
|
+
@renderBurndownChart(@tickets.models)
|
36
|
+
@
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
rerenderTickets: ->
|
41
|
+
template = HandlebarsTemplates['houston/roadmaps/milestone/ticket']
|
42
|
+
$tickets = @$el.find('#tickets')
|
43
|
+
return @render() if $tickets.length is 0
|
44
|
+
$tickets.empty()
|
45
|
+
for ticket in @tickets.models
|
46
|
+
json = ticket.toJSON()
|
47
|
+
json.estimatedEffortCompleted = ticket.estimatedEffortCompleted()
|
48
|
+
json.estimatedEffort = ticket.estimatedEffort()
|
49
|
+
$tickets.append template(json)
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
renderBurndownChart: (tickets)->
|
54
|
+
|
55
|
+
# Sum progress by week;
|
56
|
+
# Find the total amount of effort to accomplish
|
57
|
+
progressBySprint = {}
|
58
|
+
totalEffort = 0
|
59
|
+
mostRecentDataPoint = 0
|
60
|
+
for ticket in tickets
|
61
|
+
effort = +ticket.estimatedEffort()
|
62
|
+
if effort and ticket.get('firstReleaseAt')
|
63
|
+
firstReleaseAt = App.parseDate(ticket.get('firstReleaseAt'))
|
64
|
+
mostRecentDataPoint = +firstReleaseAt if mostRecentDataPoint < firstReleaseAt
|
65
|
+
sprint = @getEndOfSprint(firstReleaseAt)
|
66
|
+
progressBySprint[sprint] = (progressBySprint[sprint] || 0) + effort
|
67
|
+
totalEffort += effort
|
68
|
+
|
69
|
+
[firstSprint, lastSprint] = d3.extent(+date for date in _.keys(progressBySprint))
|
70
|
+
|
71
|
+
# Start 1 week before the first progress was made
|
72
|
+
# to show the original total effort of the milestone
|
73
|
+
firstSprint = @prevSprint(firstSprint)
|
74
|
+
|
75
|
+
# Transform into remaining effort by week:
|
76
|
+
# Iterate by week in case there are some weeks
|
77
|
+
# where no progress was made
|
78
|
+
remainingEffort = totalEffort
|
79
|
+
sprint = firstSprint
|
80
|
+
data = []
|
81
|
+
while sprint <= lastSprint
|
82
|
+
remainingEffort -= (progressBySprint[sprint] || 0)
|
83
|
+
data.push
|
84
|
+
day: new Date(sprint)
|
85
|
+
effort: Math.ceil(remainingEffort)
|
86
|
+
sprint = @nextSprint(sprint)
|
87
|
+
|
88
|
+
# If the most recent data point is for an incomplete
|
89
|
+
# sprint, disregard it when calculating the regressions
|
90
|
+
lastCompleteSprint = @getEndOfSprint(1.week().before(new Date()))
|
91
|
+
if @truncateDate(mostRecentDataPoint) > lastCompleteSprint
|
92
|
+
regAll = @computeRegression(data.slice( 0, -1)) if data.length >= 6 # all time
|
93
|
+
regLast3 = @computeRegression(data.slice(-5, -1)) if data.length >= 5 # last 3 weeks only
|
94
|
+
regLast2 = @computeRegression(data.slice(-4, -1)) if data.length >= 4 # last 2 weeks only
|
95
|
+
else
|
96
|
+
regAll = @computeRegression(data) if data.length >= 5 # all time
|
97
|
+
regLast3 = @computeRegression(data.slice(-4)) if data.length >= 4 # last 3 weeks only
|
98
|
+
regLast2 = @computeRegression(data.slice(-3)) if data.length >= 3 # last 2 weeks only
|
99
|
+
|
100
|
+
width = mostRecentDataPoint - firstSprint
|
101
|
+
maxDate = @getEndOfSprint(mostRecentDataPoint + width)
|
102
|
+
console.log 'earliestDataPoint', new Date(firstSprint)
|
103
|
+
console.log 'mostRecentDataPoint', new Date(mostRecentDataPoint)
|
104
|
+
console.log 'lastCompleteSprint', new Date(lastCompleteSprint)
|
105
|
+
console.log "width: #{(width / Duration.DAY).toFixed(1)} days"
|
106
|
+
|
107
|
+
console.log 'regAll', new Date(regAll.x2) if regAll
|
108
|
+
console.log 'regLast3', new Date(regLast3.x2) if regLast3
|
109
|
+
console.log 'regLast2', new Date(regLast2.x2) if regLast2
|
110
|
+
|
111
|
+
# Widen the graph so that it includes the X intercept
|
112
|
+
projections = []
|
113
|
+
projections.push regAll.x2 if regAll
|
114
|
+
projections.push regLast2.x2 if regLast2
|
115
|
+
projections.push regLast3.x2 if regLast3
|
116
|
+
sprints = (d.day for d in data)
|
117
|
+
if projectedEnd = projections.max()
|
118
|
+
lastSprint = @getEndOfSprint(projectedEnd)
|
119
|
+
lastSprint = maxDate if lastSprint > maxDate
|
120
|
+
sprint = _.last(sprints)
|
121
|
+
while sprint < lastSprint
|
122
|
+
sprint = @nextSprint(sprint)
|
123
|
+
sprints.push(sprint)
|
124
|
+
|
125
|
+
chart = new Houston.BurndownChart()
|
126
|
+
.days((new Date(sprint) for sprint in sprints))
|
127
|
+
.dateFormat(d3.time.format('%b %e'))
|
128
|
+
.totalEffort(totalEffort)
|
129
|
+
.addLine('completed', data)
|
130
|
+
chart.addRegression('all', regAll) if regAll
|
131
|
+
chart.addRegression('last-3', regLast3) if regLast3
|
132
|
+
chart.addRegression('last-2', regLast2) if regLast2
|
133
|
+
chart.render()
|
134
|
+
|
135
|
+
insertLinebreaks = (d)->
|
136
|
+
el = d3.select(this)
|
137
|
+
words = el.text().split(/\s+/)
|
138
|
+
el.text('')
|
139
|
+
|
140
|
+
el.append('tspan').text(words[0]).attr('class', 'month')
|
141
|
+
el.append('tspan').text(words[1]).attr('x', 0).attr('dy', '11').attr('class', 'day')
|
142
|
+
|
143
|
+
svg = d3.select('#graph').select('svg')
|
144
|
+
svg.selectAll('.x.axis text').each(insertLinebreaks)
|
145
|
+
|
146
|
+
prevSprint: (timestamp)->
|
147
|
+
1.week().before(new Date(timestamp)).getTime()
|
148
|
+
|
149
|
+
nextSprint: (timestamp)->
|
150
|
+
1.week().after(new Date(timestamp)).getTime()
|
151
|
+
|
152
|
+
getEndOfSprint: (timestamp)->
|
153
|
+
+@getNextFriday(new Date(timestamp))
|
154
|
+
|
155
|
+
getNextFriday: (date)->
|
156
|
+
wday = date.getDay() # 0-6 (0=Sunday)
|
157
|
+
daysUntilFriday = 5 - wday # 5=Friday
|
158
|
+
daysUntilFriday += 7 if daysUntilFriday < 0
|
159
|
+
daysUntilFriday.days().after(date)
|
160
|
+
|
161
|
+
truncateDate: (date)->
|
162
|
+
date = new Date(date)
|
163
|
+
date.setHours(0)
|
164
|
+
date.setMinutes(0)
|
165
|
+
date.setSeconds(0)
|
166
|
+
date.setMilliseconds(0)
|
167
|
+
+date
|
168
|
+
|
169
|
+
computeRegression: (data)->
|
170
|
+
# Compute the linear regression of the points
|
171
|
+
# http://trentrichardson.com/2010/04/06/compute-linear-regressions-in-javascript/
|
172
|
+
# http://dracoblue.net/dev/linear-least-squares-in-javascript/159/
|
173
|
+
[sum_x, sum_y, sum_xx, sum_xy, n] = [0, 0, 0, 0, data.length]
|
174
|
+
for d in data
|
175
|
+
[_x, _y] = [+d.day, d.effort]
|
176
|
+
sum_x += _x
|
177
|
+
sum_y += _y
|
178
|
+
sum_xx += _x * _x
|
179
|
+
sum_xy += _x * _y
|
180
|
+
m = (n*sum_xy - sum_x*sum_y) / (n*sum_xx - sum_x*sum_x)
|
181
|
+
b = (sum_y - m*sum_x) / n
|
182
|
+
|
183
|
+
# No progress is being made
|
184
|
+
return null if m == 0
|
185
|
+
|
186
|
+
# Find the X intercept
|
187
|
+
[x0, y0] = [((0 - b) / m), 0]
|
188
|
+
|
189
|
+
# Calculate the regression line
|
190
|
+
x1: new Date(+data[0].day)
|
191
|
+
x2: x0
|
192
|
+
y1: new Date(b + m * +data[0].day)
|
193
|
+
y2: y0
|
194
|
+
|
195
|
+
|
196
|
+
|
197
|
+
toggleShowCompleted: (e)->
|
198
|
+
$button = $(e.target)
|
199
|
+
if $button.hasClass('active')
|
200
|
+
$button.removeClass('btn-success')
|
201
|
+
@$el.addClass('hide-completed')
|
202
|
+
else
|
203
|
+
$button.addClass('btn-success')
|
204
|
+
@$el.removeClass('hide-completed')
|
@@ -0,0 +1,405 @@
|
|
1
|
+
class Roadmaps.EditGanttChart extends Roadmaps.GanttChart
|
2
|
+
MAX_BANDS = 4
|
3
|
+
$newMilestone: null
|
4
|
+
newMilestoneX: null
|
5
|
+
supportsCreate: false
|
6
|
+
|
7
|
+
|
8
|
+
constructor: (milestones, options={})->
|
9
|
+
super(milestones, options)
|
10
|
+
|
11
|
+
if options.removeMilestoneByDroppingOn
|
12
|
+
$(options.removeMilestoneByDroppingOn).droppable
|
13
|
+
tolerance: 'pointer'
|
14
|
+
drop: (e, ui) =>
|
15
|
+
return unless @drag?.milestone
|
16
|
+
@drag.milestone.markRemoved()
|
17
|
+
|
18
|
+
$(document.body).on 'keyup', (e)=>
|
19
|
+
if e.keyCode is 27
|
20
|
+
@cancelCreate() if @$newMilestone and @supportsCreate
|
21
|
+
if @drag
|
22
|
+
@update()
|
23
|
+
@drag = null
|
24
|
+
|
25
|
+
|
26
|
+
createMilestone: (callback)->
|
27
|
+
@supportsCreate = !!callback
|
28
|
+
@_createMilestoneCallback = callback
|
29
|
+
@
|
30
|
+
|
31
|
+
|
32
|
+
render: ->
|
33
|
+
super
|
34
|
+
@$el.on 'mouseleave', => @cancelCreate()
|
35
|
+
|
36
|
+
@$el.on 'mouseup', (e)=>
|
37
|
+
return unless @$newMilestone and @supportsCreate
|
38
|
+
newMilestoneWidth = @$newMilestone.outerWidth()
|
39
|
+
if newMilestoneWidth > 10
|
40
|
+
band = +@$newMilestone.closest('.roadmap-band').attr('data-band')
|
41
|
+
left = @$newMilestone.position().left
|
42
|
+
startDate = @x.invert(left)
|
43
|
+
endDate = @x.invert(left + newMilestoneWidth)
|
44
|
+
attributes =
|
45
|
+
band: band
|
46
|
+
lanes: 1
|
47
|
+
startDate: d3.time.monday.round(startDate)
|
48
|
+
endDate: d3.time.saturday.round(endDate)
|
49
|
+
@$newMilestone.addClass('creating').text('Saving...')
|
50
|
+
@$el.removeClass('drag-create')
|
51
|
+
[$newMilestone, @$newMilestone] = [@$newMilestone, null]
|
52
|
+
@_createMilestoneCallback attributes, -> $newMilestone.remove()
|
53
|
+
else
|
54
|
+
@cancelCreate()
|
55
|
+
|
56
|
+
@$el.on 'mousemove', (e)=>
|
57
|
+
return unless @$newMilestone and @supportsCreate
|
58
|
+
newMilestoneWidth = e.screenX - @newMilestoneX
|
59
|
+
newMilestoneWidth = 0 if newMilestoneWidth < 0
|
60
|
+
|
61
|
+
left = @$newMilestone.position().left
|
62
|
+
startDate = @x.invert(left)
|
63
|
+
endDate = d3.time.saturday.round(@x.invert(@$newMilestone.position().left + newMilestoneWidth))
|
64
|
+
newMilestoneWidth = @x(endDate) - left
|
65
|
+
milliseconds = endDate.getTime() - startDate.getTime() + 2 * Duration.DAY
|
66
|
+
weeks = Math.floor(milliseconds / Duration.WEEK)
|
67
|
+
|
68
|
+
@$newMilestone.css(width: newMilestoneWidth)
|
69
|
+
if weeks < 2
|
70
|
+
@$newMilestone.html(" ")
|
71
|
+
else
|
72
|
+
@$newMilestone.html("<span>#{weeks} weeks</span>")
|
73
|
+
|
74
|
+
|
75
|
+
cancelCreate: ->
|
76
|
+
return unless @$newMilestone and @supportsCreate
|
77
|
+
@$el.removeClass('drag-create')
|
78
|
+
@$newMilestone.remove()
|
79
|
+
@$newMilestone = null
|
80
|
+
|
81
|
+
|
82
|
+
initializeBand: (band)->
|
83
|
+
view = @
|
84
|
+
|
85
|
+
$(band).droppable
|
86
|
+
hoverClass: 'sort-active'
|
87
|
+
tolerance: 'pointer'
|
88
|
+
over: (e, ui)->
|
89
|
+
return unless view.drag
|
90
|
+
band = +$(@).attr('data-band')
|
91
|
+
|
92
|
+
# When we start dragging a multi-lane milestone,
|
93
|
+
# note which of its lanes we grabbed.
|
94
|
+
unless view.drag.bandOver
|
95
|
+
if view.drag.milestone
|
96
|
+
view.drag.bandOffset = band - view.drag.milestone.get('band')
|
97
|
+
else
|
98
|
+
view.drag.bandOffset = 0
|
99
|
+
|
100
|
+
view.drag.bandOver = band - view.drag.bandOffset
|
101
|
+
|
102
|
+
drop: (e, ui) =>
|
103
|
+
return unless @drag
|
104
|
+
|
105
|
+
startDate = d3.time.monday.round(@x.invert(ui.position.left))
|
106
|
+
ui.draggable.css left: @x(startDate)
|
107
|
+
|
108
|
+
if @drag.milestone
|
109
|
+
milestone = @drag.milestone
|
110
|
+
milestone.set
|
111
|
+
band: @drag.bandOver or milestone.get('band')
|
112
|
+
startDate: startDate
|
113
|
+
endDate: d3.time.saturday.round(milestone.duration().after(startDate))
|
114
|
+
|
115
|
+
@saveRepositionedMilestones()
|
116
|
+
|
117
|
+
if @drag.goal
|
118
|
+
milestone = new Roadmaps.Milestone
|
119
|
+
milestoneId: @drag.goal.id
|
120
|
+
name: @drag.goal.name
|
121
|
+
projectId: @drag.goal.projectId
|
122
|
+
projectColor: @drag.goal.projectColor
|
123
|
+
band: @drag.bandOver or 1
|
124
|
+
removed: false
|
125
|
+
lanes: 1
|
126
|
+
startDate: startDate
|
127
|
+
endDate: d3.time.saturday.round(26.days().after(startDate))
|
128
|
+
@milestones.add milestone
|
129
|
+
|
130
|
+
.on 'mousedown', (e)->
|
131
|
+
return unless view.supportsCreate
|
132
|
+
return if e.target isnt @
|
133
|
+
view.newMilestoneX = e.screenX
|
134
|
+
view.$el.addClass('drag-create')
|
135
|
+
|
136
|
+
startDate = d3.time.monday.round(view.x.invert(e.offsetX))
|
137
|
+
|
138
|
+
view.$newMilestone = $('<div class="roadmap-milestone-placeholder"> </div>')
|
139
|
+
.css(left: view.x(startDate))
|
140
|
+
.appendTo(@)
|
141
|
+
|
142
|
+
initializeMilestone: (milestone)->
|
143
|
+
view = @
|
144
|
+
$milestone = $(milestone)
|
145
|
+
$menu = null
|
146
|
+
|
147
|
+
$milestone.pseudoHover()
|
148
|
+
$milestone.attr "tabindex", -1
|
149
|
+
$milestone.blur (e)=>
|
150
|
+
if $menu
|
151
|
+
# Don't remove the menu before the click event
|
152
|
+
# propagates to the menu item that was clicked!
|
153
|
+
window.setTimeout (=>
|
154
|
+
$milestone.removeClass "dropdown-open"
|
155
|
+
$menu.remove()
|
156
|
+
$menu = null), 100
|
157
|
+
$milestone.on 'contextmenu', (e)=>
|
158
|
+
return false if $menu
|
159
|
+
$milestone.focus()
|
160
|
+
$milestone.addClass "dropdown-open"
|
161
|
+
id = $milestone.attr('data-milestone-id')
|
162
|
+
html = """
|
163
|
+
<ul class="dropdown-menu" role="menu">
|
164
|
+
"""
|
165
|
+
html += """
|
166
|
+
<li role="presentation"><a role="menuitem" tabindex="-1" href="/roadmap/milestones/#{id}" target="_blank">Open</a></li>
|
167
|
+
""" if id
|
168
|
+
html += """
|
169
|
+
<li role="presentation"><a role="menuitem" tabindex="-1" class="rename-milestone-action">Rename</a></li>
|
170
|
+
<li role="presentation"><a role="menuitem" tabindex="-1" class="remove-milestone-action">Remove</a></li>
|
171
|
+
</ul>
|
172
|
+
"""
|
173
|
+
$menu = $(html)
|
174
|
+
.appendTo(document.body)
|
175
|
+
.css
|
176
|
+
display: "block"
|
177
|
+
left: $milestone.offset().left
|
178
|
+
top: $milestone.offset().top + $milestone.outerHeight() + 2
|
179
|
+
|
180
|
+
.on "click", ".rename-milestone-action", (e)=>
|
181
|
+
id = $milestone.attr('data-cid')
|
182
|
+
milestone = @milestones.get(id)
|
183
|
+
return false unless milestone
|
184
|
+
|
185
|
+
if name = prompt('Name:', milestone.get('name'))
|
186
|
+
milestone.set(name: name)
|
187
|
+
|
188
|
+
.on "click", ".remove-milestone-action", (e)=>
|
189
|
+
id = $milestone.attr('data-cid')
|
190
|
+
milestone = @milestones.get(id)
|
191
|
+
return false unless milestone
|
192
|
+
|
193
|
+
milestone.markRemoved()
|
194
|
+
$milestone.remove()
|
195
|
+
false
|
196
|
+
|
197
|
+
$(milestone).resizable
|
198
|
+
handles: 'e, s, se'
|
199
|
+
animate: false
|
200
|
+
start: _.bind(@onStartDrag, @)
|
201
|
+
|
202
|
+
resize: (e, ui)=>
|
203
|
+
return unless @drag?.milestone
|
204
|
+
|
205
|
+
# If we're _only_ changing the number of lanes
|
206
|
+
# this milestone covers, update its duration as
|
207
|
+
# its height is changed.
|
208
|
+
if @drag.handle is 'ui-resizable-s'
|
209
|
+
lanes = @lanesSpanned(ui.size.height)
|
210
|
+
if lanes isnt @drag.currentLanes
|
211
|
+
@drag.currentLanes = lanes
|
212
|
+
originalLanes = @lanesSpanned(ui.originalSize.height)
|
213
|
+
ui.size.width = ui.originalSize.width * Math.pow(2, originalLanes - lanes)
|
214
|
+
$(ui.element).css(width: ui.size.width)
|
215
|
+
@initializeDrag()
|
216
|
+
|
217
|
+
@repositionMilestones ui.size.width - ui.originalSize.width
|
218
|
+
|
219
|
+
stop: (e, ui)=>
|
220
|
+
ui.element.resizable 'option', 'grid', false
|
221
|
+
return unless @drag?.milestone
|
222
|
+
|
223
|
+
milestone = @drag.milestone
|
224
|
+
endDate = d3.time.saturday.round(@x.invert(ui.position.left + ui.size.width))
|
225
|
+
ui.element.css width: @x(endDate) - ui.element.position().left
|
226
|
+
lanes = @lanesSpanned(ui.size.height)
|
227
|
+
milestone.set
|
228
|
+
endDate: endDate
|
229
|
+
lanes: lanes
|
230
|
+
|
231
|
+
@saveRepositionedMilestones()
|
232
|
+
@drag = null
|
233
|
+
|
234
|
+
.draggable
|
235
|
+
snap: '.roadmap-band'
|
236
|
+
snapMode: 'inner'
|
237
|
+
zIndex: 10
|
238
|
+
revertDuration: 150
|
239
|
+
start: _.bind(@onStartDrag, @)
|
240
|
+
|
241
|
+
drag: (e, ui)=>
|
242
|
+
return false unless @drag?.milestone
|
243
|
+
@initializeDrag() unless @drag.band is @drag.bandOver
|
244
|
+
|
245
|
+
ui.position.left = Math.max(ui.position.left, @drag.minLeft)
|
246
|
+
|
247
|
+
if e.shiftKey
|
248
|
+
if @drag.maxRight
|
249
|
+
width = $(e.target).outerWidth()
|
250
|
+
ui.position.left = Math.min(ui.position.left, @drag.maxRight - width)
|
251
|
+
delta = 0
|
252
|
+
else
|
253
|
+
delta = ui.position.left - ui.originalPosition.left
|
254
|
+
|
255
|
+
@repositionMilestones(delta)
|
256
|
+
|
257
|
+
stop: (e, ui)=>
|
258
|
+
@drag = null
|
259
|
+
$(e.target).resizable 'option', 'grid', false
|
260
|
+
|
261
|
+
revert: ($target)->
|
262
|
+
!$target or !$target.is('.roadmap-band')
|
263
|
+
|
264
|
+
|
265
|
+
dragFrom: ($selector, goals) ->
|
266
|
+
$selector.draggable
|
267
|
+
snap: '.roadmap-band'
|
268
|
+
snapMode: 'inner'
|
269
|
+
zIndex: 10
|
270
|
+
|
271
|
+
# Position of the cursor over the drag helper
|
272
|
+
cursorAt:
|
273
|
+
top: 15
|
274
|
+
left: 50
|
275
|
+
|
276
|
+
# Append the helper to `body` so that it isn't clipped
|
277
|
+
# by its container (like if its container is scrollable)
|
278
|
+
appendTo: document.body
|
279
|
+
|
280
|
+
revertDuration: 150
|
281
|
+
|
282
|
+
helper: (e) =>
|
283
|
+
$goal = $(e.target).closest('.goal')
|
284
|
+
id = $goal.data('id')
|
285
|
+
goal = goals.get(id)
|
286
|
+
@drag = {goal: goal.toJSON()}
|
287
|
+
|
288
|
+
$ """
|
289
|
+
<div class="roadmap-milestone unlocked uncompleted unhovered #{goal.get('projectColor')}" tabindex="-1">
|
290
|
+
<span class="roadmap-milestone-name">#{goal.get('name')}</span>
|
291
|
+
</div>
|
292
|
+
"""
|
293
|
+
|
294
|
+
stop: (e, ui) =>
|
295
|
+
@drag = null
|
296
|
+
|
297
|
+
|
298
|
+
onStartDrag: (e, ui)->
|
299
|
+
$milestone = $(e.target)
|
300
|
+
return false if $milestone.hasClass('locked')
|
301
|
+
|
302
|
+
handle = $(e.originalEvent.target).attr('class')
|
303
|
+
handle = handle.split(' ')[1] if handle
|
304
|
+
|
305
|
+
id = $milestone.attr('data-cid')
|
306
|
+
milestone = @milestones.get(id)
|
307
|
+
|
308
|
+
grid = [@weekWidth(), @bandHeight + @bandMargin]
|
309
|
+
$milestone.resizable 'option', 'grid', grid
|
310
|
+
|
311
|
+
$('.roadmap-milestone').each ->
|
312
|
+
$milestone = $(@)
|
313
|
+
$milestone.data('original-left', $milestone.position().left)
|
314
|
+
|
315
|
+
@drag =
|
316
|
+
milestone: milestone
|
317
|
+
handle: handle
|
318
|
+
|
319
|
+
@initializeDrag()
|
320
|
+
|
321
|
+
|
322
|
+
initializeDrag: ->
|
323
|
+
milestone = @drag.milestone
|
324
|
+
return unless milestone
|
325
|
+
|
326
|
+
band = @drag.bandOver || milestone.get('band')
|
327
|
+
lanes = @drag.currentLanes || milestone.get('lanes')
|
328
|
+
bands = (band + i for i in [0...lanes])
|
329
|
+
|
330
|
+
milestonesInBand = @milestones.overlappingBands(bands)
|
331
|
+
minStartDate = 2.days().after(milestonesInBand.lastMilestoneBefore(milestone)?.get('endDate'))
|
332
|
+
maxEndDate = 2.days().before(milestonesInBand.firstMilestoneAfter(milestone)?.get('startDate'))
|
333
|
+
|
334
|
+
milestonesAfter = @milestones.downstreamOf(milestone.get('endDate'), bands)
|
335
|
+
$milestonesAfter = @$el.find(milestonesAfter
|
336
|
+
.map (m)-> ".roadmap-milestone[data-cid=#{m.cid}]"
|
337
|
+
.join(', '))
|
338
|
+
|
339
|
+
@drag = @drag extends
|
340
|
+
band: band
|
341
|
+
minLeft: if minStartDate then @x(minStartDate) else 0
|
342
|
+
maxRight: maxEndDate && @x(maxEndDate)
|
343
|
+
$milestonesAfter: $milestonesAfter
|
344
|
+
|
345
|
+
|
346
|
+
repositionMilestones: (delta)->
|
347
|
+
return unless @drag?.$milestonesAfter
|
348
|
+
|
349
|
+
# !todo: check for unmoveable milestones
|
350
|
+
@drag.$milestonesAfter.each ->
|
351
|
+
$milestone = $(@)
|
352
|
+
originalLeft = +$milestone.data('original-left')
|
353
|
+
$milestone.css(left: originalLeft + delta)
|
354
|
+
|
355
|
+
saveRepositionedMilestones: ->
|
356
|
+
return unless @drag?.$milestonesAfter
|
357
|
+
|
358
|
+
@drag.$milestonesAfter.each (i, el)=>
|
359
|
+
$milestone = $(el)
|
360
|
+
milestone = @milestones.get $milestone.data('cid')
|
361
|
+
|
362
|
+
unless milestone
|
363
|
+
App.debug 'There is no milestone for', $milestone
|
364
|
+
return
|
365
|
+
|
366
|
+
if milestone.get('locked')
|
367
|
+
App.debug "#{milestone.get('name')} is locked and cannot be repositioned"
|
368
|
+
return
|
369
|
+
|
370
|
+
newPosition = $milestone.position().left
|
371
|
+
startDate = d3.time.monday.round(@x.invert(newPosition))
|
372
|
+
endDate = d3.time.saturday.round(milestone.duration().after(startDate))
|
373
|
+
milestone.set
|
374
|
+
startDate: startDate
|
375
|
+
endDate: endDate
|
376
|
+
|
377
|
+
|
378
|
+
|
379
|
+
groupMilestonesIntoBands: ->
|
380
|
+
milestoneBands = super
|
381
|
+
|
382
|
+
# Make sure there is at least one empty band in the Roadmap
|
383
|
+
# (up to MAX_BANDS); then don't add an empty band.
|
384
|
+
nextBand = +d3.max(milestoneBands, (band)-> band.number) + 1
|
385
|
+
nextBand = 1 if _.isNaN(nextBand)
|
386
|
+
if _.keys(milestoneBands).length <= MAX_BANDS
|
387
|
+
milestoneBands.push
|
388
|
+
key: nextBand
|
389
|
+
number: nextBand
|
390
|
+
milestones: []
|
391
|
+
|
392
|
+
milestoneBands
|
393
|
+
|
394
|
+
weekWidth: ->
|
395
|
+
date = d3.time.saturdays(@x.domain()...)[0]
|
396
|
+
@x(7.days().after(date)) - @x(date)
|
397
|
+
|
398
|
+
lanesSpanned: (height)->
|
399
|
+
# space between lanes is 8; height if lane + space is 38
|
400
|
+
(height + 8) / 38
|
401
|
+
|
402
|
+
toJSON: (milestone)->
|
403
|
+
json = milestone.toJSON()
|
404
|
+
json.cid = milestone.cid # Use `cid` because some milestones may not be saved yet
|
405
|
+
json
|