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
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
begin
|
2
|
+
require "bundler/setup"
|
3
|
+
rescue LoadError
|
4
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
# `rake test` and `rake test:all` are loaded from Houston
|
9
|
+
require_relative "test/dummy/houston"
|
10
|
+
Houston::Application.load_tasks
|
11
|
+
|
12
|
+
|
13
|
+
require "rdoc/task"
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = "rdoc"
|
17
|
+
rdoc.title = "Roadmap"
|
18
|
+
rdoc.options << "--line-numbers"
|
19
|
+
rdoc.rdoc_files.include("README.rdoc")
|
20
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
Bundler::GemHelper.install_tasks
|
25
|
+
|
File without changes
|
@@ -0,0 +1,14 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// the compiled file.
|
9
|
+
//
|
10
|
+
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
11
|
+
// GO AFTER THE REQUIRES BELOW.
|
12
|
+
//
|
13
|
+
//= require_tree ../../../templates/houston/roadmaps
|
14
|
+
//= require_tree .
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Handlebars.registerPartial 'milestoneTicket', (task)->
|
2
|
+
HandlebarsTemplates['houston/roadmaps/milestone/ticket'](task)
|
3
|
+
|
4
|
+
Handlebars.registerHelper 'durationOfMilestone', (milestone)->
|
5
|
+
return "" unless milestone.endDate and milestone.startDate
|
6
|
+
duration = milestone.endDate - milestone.startDate
|
7
|
+
weeks = Math.round(duration / Duration.WEEK)
|
8
|
+
"#{weeks} weeks"
|
9
|
+
|
10
|
+
Handlebars.registerHelper 'milestonePercentComplete', (milestone)->
|
11
|
+
return "" if !milestone.percentComplete?
|
12
|
+
App.formatPercent milestone.percentComplete
|
13
|
+
|
14
|
+
Handlebars.registerHelper 'ticketPercentComplete', (ticket)->
|
15
|
+
return "" unless ticket.estimatedEffort? and ticket.estimatedEffortCompleted?
|
16
|
+
percent = ticket.estimatedEffortCompleted / ticket.estimatedEffort
|
17
|
+
App.formatPercent percent
|
18
|
+
|
19
|
+
Handlebars.registerHelper 'linkToFeedbackQuery', (projectSlug, feedbackQuery)->
|
20
|
+
q = encodeURIComponent(feedbackQuery)
|
21
|
+
path = "/feedback/by_project/#{projectSlug}?q=#{q}"
|
22
|
+
"<a href=\"#{path}\">See comments</a>"
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Roadmaps.Goal extends Backbone.Model
|
2
|
+
urlRoot: '/roadmap/milestones'
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
class Roadmaps.Goals extends Backbone.Collection
|
7
|
+
model: Roadmaps.Goal
|
8
|
+
|
9
|
+
outsideOf: (milestones) ->
|
10
|
+
ids = _(milestones.pluck('milestoneId'))
|
11
|
+
new Roadmaps.Goals @reject (goal) -> ids.include(goal.id)
|
@@ -0,0 +1,124 @@
|
|
1
|
+
class Roadmaps.Milestone extends Backbone.Model
|
2
|
+
urlRoot: '/roadmap/milestones'
|
3
|
+
|
4
|
+
initialize: ->
|
5
|
+
super
|
6
|
+
_.bindAll(@, 'clearChangesSinceSave', 'revert')
|
7
|
+
@clearChangesSinceSave()
|
8
|
+
|
9
|
+
save: (attrs, options)->
|
10
|
+
success = options?.success
|
11
|
+
options.success = (response)=>
|
12
|
+
@trigger('save:success', @)
|
13
|
+
success(@, response) if success
|
14
|
+
@clearChangesSinceSave()
|
15
|
+
@trigger('save', @)
|
16
|
+
super
|
17
|
+
|
18
|
+
duration: ->
|
19
|
+
Math.floor((@get('endDate') - @get('startDate')) / Duration.DAY).days()
|
20
|
+
|
21
|
+
clearChangesSinceSave: ->
|
22
|
+
@_originalAttributes = _.clone @attributes
|
23
|
+
@_originalAttributes.removed = false
|
24
|
+
|
25
|
+
revert: ->
|
26
|
+
@set(@_originalAttributes) if @_originalAttributes
|
27
|
+
|
28
|
+
markRemoved: ->
|
29
|
+
@set(removed: true)
|
30
|
+
|
31
|
+
changesSinceSave: ->
|
32
|
+
changes = {}
|
33
|
+
for attribute, value of @attributes
|
34
|
+
originalValue = @_originalAttributes[attribute]
|
35
|
+
changes[attribute] = [originalValue, value] unless _.isEqual(originalValue, value)
|
36
|
+
changes
|
37
|
+
|
38
|
+
bands: ->
|
39
|
+
band = @get('band')
|
40
|
+
bands = [band]
|
41
|
+
for i in [1...@get('lanes')]
|
42
|
+
bands.push band + i
|
43
|
+
bands
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
parse: (milestone)->
|
48
|
+
milestone.startDate = App.serverDateFormat.parse(milestone.startDate) if milestone.startDate and !_.isDate(milestone.startDate)
|
49
|
+
milestone.endDate = App.serverDateFormat.parse(milestone.endDate) if milestone.endDate and !_.isDate(milestone.endDate)
|
50
|
+
milestone
|
51
|
+
|
52
|
+
toJSON: (options)->
|
53
|
+
json = super(options)
|
54
|
+
json.cid = @cid
|
55
|
+
if 'emulateHTTP' of (options || {})
|
56
|
+
json.start_date = App.serverDateFormat(json.startDate) if json.startDate
|
57
|
+
delete json.startDate
|
58
|
+
json.end_date = App.serverDateFormat(json.endDate) if json.endDate
|
59
|
+
delete json.endDate
|
60
|
+
json
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
class Roadmaps.Milestones extends Backbone.Collection
|
65
|
+
model: Roadmaps.Milestone
|
66
|
+
comparator: 'startDate'
|
67
|
+
|
68
|
+
start: -> _.min @pluck('startDate')
|
69
|
+
end: -> _.max @pluck('endDate')
|
70
|
+
|
71
|
+
revert: ->
|
72
|
+
i = 0
|
73
|
+
while i < @length
|
74
|
+
milestone = @models[i]
|
75
|
+
if milestone.get('id')
|
76
|
+
milestone.revert()
|
77
|
+
i++
|
78
|
+
else
|
79
|
+
@remove(milestone)
|
80
|
+
|
81
|
+
changes: ->
|
82
|
+
for milestone in @models when !milestone.id or _.keys(changes = milestone.changesSinceSave()).length > 0
|
83
|
+
if milestone.id
|
84
|
+
change = id: milestone.id
|
85
|
+
for attribute, [originalView, newValue] of changes
|
86
|
+
[attribute, newValue] = ['start_date', App.serverDateFormat(newValue)] if attribute is 'startDate'
|
87
|
+
[attribute, newValue] = ['end_date', App.serverDateFormat(newValue)] if attribute is 'endDate'
|
88
|
+
change[attribute] = newValue
|
89
|
+
change
|
90
|
+
else
|
91
|
+
milestone.toJSON(emulateHTTP: true)
|
92
|
+
|
93
|
+
overlappingBands: (bands)->
|
94
|
+
new Milestones @select (m)-> _.intersection(m.bands(), bands).length > 0
|
95
|
+
|
96
|
+
before: (milestone)->
|
97
|
+
startDate = milestone.get('startDate')
|
98
|
+
new Milestones @select (m)-> m.get('endDate') < startDate
|
99
|
+
|
100
|
+
after: (milestone)->
|
101
|
+
endDate = milestone.get('endDate')
|
102
|
+
new Milestones @select (m)-> m.get('startDate') > endDate
|
103
|
+
|
104
|
+
lastMilestoneBefore: (milestone)->
|
105
|
+
@before(milestone).sortBy('endDate').last()
|
106
|
+
|
107
|
+
firstMilestoneAfter: (milestone)->
|
108
|
+
@after(milestone).sortBy('startDate').first()
|
109
|
+
|
110
|
+
downstreamOf: (date, bands)->
|
111
|
+
milestones = []
|
112
|
+
@each (milestone)->
|
113
|
+
return if milestone.get('startDate') < date
|
114
|
+
newBands = milestone.bands()
|
115
|
+
return if _.intersection(newBands, bands).length is 0
|
116
|
+
|
117
|
+
# If this milestone being dragged can push this milestone,
|
118
|
+
# then it can also push any milestones this one can push
|
119
|
+
bands = _.union(bands, newBands)
|
120
|
+
milestones.push milestone
|
121
|
+
milestones
|
122
|
+
|
123
|
+
unremoved: ->
|
124
|
+
new Roadmaps.Milestones @where(removed: false)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class @Roadmaps.Roadmap extends Backbone.Model
|
2
|
+
urlRoot: "/roadmaps"
|
3
|
+
|
4
|
+
milestones: ->
|
5
|
+
@_milestones ||= new Roadmaps.Milestones(@get("milestones"), parse: true)
|
6
|
+
|
7
|
+
viewport: ->
|
8
|
+
new Roadmaps.Viewport
|
9
|
+
start: @milestones().start()
|
10
|
+
end: @milestones().end()
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
class @Roadmaps.Roadmaps extends Backbone.Collection
|
15
|
+
model: window.Roadmaps.Roadmap
|
16
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
// This is kind of a hack that allows you to execute code
|
2
|
+
// when an element is resized. See:
|
3
|
+
// http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/
|
4
|
+
(function(){
|
5
|
+
var attachEvent = document.attachEvent;
|
6
|
+
var isIE = navigator.userAgent.match(/Trident/);
|
7
|
+
var requestFrame = (function(){
|
8
|
+
var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
|
9
|
+
function(fn){ return window.setTimeout(fn, 20); };
|
10
|
+
return function(fn){ return raf(fn); };
|
11
|
+
})();
|
12
|
+
|
13
|
+
var cancelFrame = (function(){
|
14
|
+
var cancel = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame ||
|
15
|
+
window.clearTimeout;
|
16
|
+
return function(id){ return cancel(id); };
|
17
|
+
})();
|
18
|
+
|
19
|
+
function resizeListener(e){
|
20
|
+
var win = e.target || e.srcElement;
|
21
|
+
if (win.__resizeRAF__) cancelFrame(win.__resizeRAF__);
|
22
|
+
win.__resizeRAF__ = requestFrame(function(){
|
23
|
+
var trigger = win.__resizeTrigger__;
|
24
|
+
trigger.__resizeListeners__.forEach(function(fn){
|
25
|
+
fn.call(trigger, e);
|
26
|
+
});
|
27
|
+
});
|
28
|
+
}
|
29
|
+
|
30
|
+
function objectLoad(e){
|
31
|
+
this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__;
|
32
|
+
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
|
33
|
+
}
|
34
|
+
|
35
|
+
window.addResizeListener = function(element, fn){
|
36
|
+
if (!element.__resizeListeners__) {
|
37
|
+
element.__resizeListeners__ = [];
|
38
|
+
if (attachEvent) {
|
39
|
+
element.__resizeTrigger__ = element;
|
40
|
+
element.attachEvent('onresize', resizeListener);
|
41
|
+
}
|
42
|
+
else {
|
43
|
+
if (getComputedStyle(element).position == 'static') element.style.position = 'relative';
|
44
|
+
var obj = element.__resizeTrigger__ = document.createElement('object');
|
45
|
+
obj.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
|
46
|
+
obj.__resizeElement__ = element;
|
47
|
+
obj.onload = objectLoad;
|
48
|
+
obj.type = 'text/html';
|
49
|
+
if (isIE) element.appendChild(obj);
|
50
|
+
obj.data = 'about:blank';
|
51
|
+
if (!isIE) element.appendChild(obj);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
element.__resizeListeners__.push(fn);
|
55
|
+
};
|
56
|
+
|
57
|
+
window.removeResizeListener = function(element, fn){
|
58
|
+
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
|
59
|
+
if (!element.__resizeListeners__.length) {
|
60
|
+
if (attachEvent) element.detachEvent('onresize', resizeListener);
|
61
|
+
else {
|
62
|
+
element.__resizeTrigger__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
|
63
|
+
element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
})();
|
@@ -0,0 +1,224 @@
|
|
1
|
+
class Roadmaps.GanttChart
|
2
|
+
today: new Date()
|
3
|
+
|
4
|
+
constructor: (@milestones, options={})->
|
5
|
+
@el = options.el
|
6
|
+
@selector = options.selector ? '#roadmap'
|
7
|
+
@showToday = options.showToday ? true
|
8
|
+
@showThumbnail = options.showThumbnail ? true
|
9
|
+
@showWeekends = options.showWeekends ? false
|
10
|
+
@linkMilestones = options.linkMilestones ? false
|
11
|
+
@showProgress = options.showProgress ? false
|
12
|
+
@bandHeight = options.bandHeight ? 30
|
13
|
+
@bandMargin = options.bandMargin ? 8
|
14
|
+
@viewport = options.viewport ? @defaultViewport()
|
15
|
+
@viewport.bind 'change', @updateViewport, @
|
16
|
+
@height = 24
|
17
|
+
@markers = options.markers ? []
|
18
|
+
for marker in @markers
|
19
|
+
marker.date = App.serverDateFormat.parse(marker.date).endOfDay()
|
20
|
+
@milestones.bind 'add', @update, @
|
21
|
+
@milestones.bind 'change', @update, @
|
22
|
+
@milestones.bind 'remove', @update, @
|
23
|
+
@milestones.bind 'reset', @update, @
|
24
|
+
$(window).resize (e)=>
|
25
|
+
@updateWindow() if e.target is window
|
26
|
+
|
27
|
+
defaultViewport: ->
|
28
|
+
new Roadmaps.Viewport
|
29
|
+
start: 3.weeks().before(@today)
|
30
|
+
end: 6.months().after(3.weeks().before(@today))
|
31
|
+
|
32
|
+
render: ->
|
33
|
+
@el = $(@selector)[0] unless @el
|
34
|
+
@$el = $(@el)
|
35
|
+
|
36
|
+
if @showThumbnail
|
37
|
+
@thumbnail = new Roadmaps.ThumbnailGanttChart
|
38
|
+
milestones: @milestones
|
39
|
+
markers: @markers
|
40
|
+
showToday: @showToday
|
41
|
+
viewport: @viewport
|
42
|
+
parent: d3.select(@el)
|
43
|
+
@thumbnail.render()
|
44
|
+
|
45
|
+
@roadmap = d3.select(@el)
|
46
|
+
.append('div')
|
47
|
+
.attr('class', 'roadmap-bands')
|
48
|
+
|
49
|
+
svg = @roadmap.append('svg')
|
50
|
+
.attr('height', @height)
|
51
|
+
.attr('class', 'roadmap-axis')
|
52
|
+
.append('g')
|
53
|
+
.attr('transform', "translate(0,0)")
|
54
|
+
@xAxis = svg.append('g').attr('class', 'x axis')
|
55
|
+
|
56
|
+
@updateWindow()
|
57
|
+
|
58
|
+
updateWindow: ->
|
59
|
+
width = @$el.width() || 960
|
60
|
+
return if @width is width
|
61
|
+
@width = width
|
62
|
+
|
63
|
+
@roadmap.select('svg').transition(150).attr('width', @width)
|
64
|
+
|
65
|
+
@x = d3.time.scale()
|
66
|
+
.domain(@viewport.domain())
|
67
|
+
.range([0, @width])
|
68
|
+
timeline = d3.svg.axis()
|
69
|
+
.scale(@x)
|
70
|
+
.orient('bottom')
|
71
|
+
@xAxis.transition(150).call(timeline)
|
72
|
+
|
73
|
+
@update(transition: true)
|
74
|
+
|
75
|
+
updateViewport: ->
|
76
|
+
@x = d3.time.scale()
|
77
|
+
.domain(@viewport.domain())
|
78
|
+
.range([0, @width])
|
79
|
+
timeline = d3.svg.axis()
|
80
|
+
.scale(@x)
|
81
|
+
.orient('bottom')
|
82
|
+
@xAxis.call(timeline)
|
83
|
+
|
84
|
+
@update(transition: false)
|
85
|
+
|
86
|
+
update: (options)->
|
87
|
+
transition = options?.transition ? true
|
88
|
+
view = @
|
89
|
+
@today = new Date()
|
90
|
+
|
91
|
+
bands = @roadmap.selectAll('.roadmap-band')
|
92
|
+
.data(@groupMilestonesIntoBands(), (band)-> band.key)
|
93
|
+
|
94
|
+
bands.enter()
|
95
|
+
.append('div')
|
96
|
+
.attr('class', (band)-> "roadmap-band")
|
97
|
+
.attr('style', (date)=> "height: #{@bandHeight}px; margin: #{@bandMargin}px 0;")
|
98
|
+
.attr('data-band', (band)-> band.number)
|
99
|
+
.each -> view.initializeBand.apply(view, [@])
|
100
|
+
|
101
|
+
bands.exit().remove()
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
if @showWeekends
|
106
|
+
weeks = @roadmap.selectAll('.roadmap-weekend')
|
107
|
+
.data(d3.time.saturdays(@x.domain()...), (date)-> date)
|
108
|
+
|
109
|
+
weeks.enter().append('div')
|
110
|
+
.attr('class', 'roadmap-weekend')
|
111
|
+
.attr('style', (date)=> "left: #{@x(date)}px; width: #{@x(2.days().after(date)) - @x(date)}px;")
|
112
|
+
|
113
|
+
update = if transition then weeks.transition(150) else weeks
|
114
|
+
update
|
115
|
+
.attr('style', (date)=> "left: #{@x(date)}px; width: #{@x(2.days().after(date)) - @x(date)}px;")
|
116
|
+
|
117
|
+
weeks.exit().remove()
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
milestones = bands.selectAll('.roadmap-milestone')
|
122
|
+
.data(((band)-> band.milestones), (milestone)-> milestone.cid)
|
123
|
+
|
124
|
+
newMilestones = if @linkMilestones
|
125
|
+
milestones.enter().append('a')
|
126
|
+
.attr('href', (milestone)-> "/roadmap/milestones/#{milestone.milestoneId}")
|
127
|
+
else
|
128
|
+
milestones.enter().append('div')
|
129
|
+
|
130
|
+
if @showProgress
|
131
|
+
newMilestones.append('div')
|
132
|
+
.attr('class', 'roadmap-milestone-progress')
|
133
|
+
|
134
|
+
newMilestones
|
135
|
+
.attr 'style', (milestone)=>
|
136
|
+
[ "left: #{@x(milestone.startDate)}px",
|
137
|
+
"width: #{@x(milestone.endDate) - @x(milestone.startDate)}px",
|
138
|
+
"height: #{milestone.lanes * (@bandHeight + @bandMargin) - @bandMargin}px" ].join('; ')
|
139
|
+
.attr('class', 'roadmap-milestone')
|
140
|
+
.attr('data-cid', (milestone)-> milestone.cid)
|
141
|
+
.attr('data-milestone-id', (milestone)-> milestone.milestoneId)
|
142
|
+
.each -> view.initializeMilestone.apply(view, [@])
|
143
|
+
|
144
|
+
# Put the milestone name into a span so that Midori can render it correctly
|
145
|
+
.append('span')
|
146
|
+
.attr('class', 'roadmap-milestone-name')
|
147
|
+
.text((milestone)-> milestone.name)
|
148
|
+
|
149
|
+
update = if transition then milestones.transition(150) else milestones
|
150
|
+
update
|
151
|
+
.attr 'class', (milestone)=>
|
152
|
+
classes = ['roadmap-milestone', milestone.projectColor]
|
153
|
+
classes.push(if milestone.locked then 'locked' else 'unlocked')
|
154
|
+
classes.push(if milestone.completed then 'completed' else 'uncompleted')
|
155
|
+
classes.push('clickable') if @linkMilestones
|
156
|
+
if milestone.startDate > @today
|
157
|
+
classes.push 'upcoming'
|
158
|
+
else if milestone.endDate < @today
|
159
|
+
classes.push 'past'
|
160
|
+
else
|
161
|
+
classes.push 'active'
|
162
|
+
classes.join(' ')
|
163
|
+
.attr 'style', (milestone)=>
|
164
|
+
[ "left: #{@x(milestone.startDate)}px",
|
165
|
+
"width: #{@x(milestone.endDate) - @x(milestone.startDate)}px",
|
166
|
+
"height: #{milestone.lanes * (@bandHeight + @bandMargin) - @bandMargin}px" ].join('; ')
|
167
|
+
.select('.roadmap-milestone-progress')
|
168
|
+
.attr 'style', (milestone)->
|
169
|
+
return "width: 0" if milestone.tickets is 0
|
170
|
+
"width: #{milestone.percentComplete * 100}%"
|
171
|
+
|
172
|
+
update.select('.roadmap-milestone-name')
|
173
|
+
.text((milestone)-> milestone.name)
|
174
|
+
|
175
|
+
milestones.exit().remove()
|
176
|
+
|
177
|
+
|
178
|
+
|
179
|
+
if @showToday
|
180
|
+
todayLine = @roadmap.selectAll('.roadmap-today')
|
181
|
+
.data([@today])
|
182
|
+
|
183
|
+
todayLine.enter()
|
184
|
+
.append('div')
|
185
|
+
.attr('class', 'roadmap-today')
|
186
|
+
.attr('style', (date)=> "left: #{@x(date)}px;")
|
187
|
+
|
188
|
+
update = if transition then todayLine.transition(150) else todayLine
|
189
|
+
update
|
190
|
+
.attr('style', (date)=> "left: #{@x(date)}px;")
|
191
|
+
|
192
|
+
|
193
|
+
|
194
|
+
markers = @roadmap.selectAll('.roadmap-marker')
|
195
|
+
.data(@markers)
|
196
|
+
|
197
|
+
markers.enter()
|
198
|
+
.append('div')
|
199
|
+
.attr('class', 'roadmap-marker')
|
200
|
+
.attr('style', ({date})=> "left: #{@x(date)}px;")
|
201
|
+
|
202
|
+
update = if transition then markers.transition(150) else markers
|
203
|
+
update
|
204
|
+
.attr('style', ({date})=> "left: #{@x(date)}px;")
|
205
|
+
|
206
|
+
markers.exit().remove()
|
207
|
+
|
208
|
+
groupMilestonesIntoBands: ->
|
209
|
+
milestoneBands = {}
|
210
|
+
milestones = (@toJSON(milestone) for milestone in @milestones.models)
|
211
|
+
for milestone in milestones when milestone.startDate and milestone.endDate and !milestone.removed
|
212
|
+
(milestoneBands[milestone.band] ||=
|
213
|
+
key: milestone.band
|
214
|
+
number: milestone.band
|
215
|
+
milestones: []).milestones.push(milestone)
|
216
|
+
_.values(milestoneBands)
|
217
|
+
|
218
|
+
toJSON: (milestone)->
|
219
|
+
json = milestone.toJSON()
|
220
|
+
json.cid = milestone.id # Use `id` because the view is readonly
|
221
|
+
json
|
222
|
+
|
223
|
+
initializeBand: ->
|
224
|
+
initializeMilestone: ->
|