Created
July 17, 2012 16:50
-
-
Save ferentchak/3130553 to your computer and use it in GitHub Desktop.
Planning Board
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |
<!-- Copyright (c) 2010 Rally Software Development Corp. All rights reserved --> | |
<html> | |
<head> | |
<title>Planning Board</title> | |
<meta name="Name" content="App: Planning Board"/> | |
<meta name="Version" content="2011.02.24"/> | |
<meta name="Vendor" content="Rally Software Labs"/> | |
<script type="text/javascript" src="/apps/1.24/sdk.js?debug=true"></script> | |
<script type="text/javascript"> | |
/** | |
Copyright (c) 2011 Rally Software Development Corp. All rights reserved | |
*/ | |
var PlanningBoardCardRenderer = function(column, item) { | |
rally.sdk.ui.cardboard.StandardCardRenderer.call(this, column, item); | |
var that = this; | |
this._linkMouseOver = function(event) { | |
var headerElement = event.srcElement; | |
var timer; | |
var delay = 1000; | |
var tooltip; | |
var link; | |
var cancelled; | |
function showTooltip(result) { | |
//var tooltipDiv = document.createElement("div"); | |
// headerElement.appendChild(tooltipDiv); | |
// attach to the right of the formatted id | |
if(!cancelled) { | |
tooltip = rally.sdk.ui.basic.Tooltip.show(headerElement, | |
result.Description || "(No descripition)", "after"); | |
} | |
} | |
function removeTooltip() { | |
if (tooltip && tooltip.hide) { | |
dojo.disconnect(link); | |
tooltip.hide(); | |
} | |
clearTimeout(timer); | |
cancelled = true; | |
} | |
function getDescription() { | |
PlanningBoardCardRenderer.rallyDataSource.getRallyObject(item, showTooltip, null, {fetch:"Description"}); | |
} | |
link = dojo.connect(headerElement, "onmouseout", removeTooltip); | |
timer = setTimeout(getDescription, delay); | |
}; | |
/* override to add plan estimate in header and show the description tooltip when hovered on the link */ | |
var parentRenderCard = this.renderCard; | |
this.renderCard = function(){ | |
var card = parentRenderCard.apply(this, arguments); | |
var header = dojo.query('.leftCardHeader', card)[0]; | |
var cardContent = dojo.query('.cardContent', card)[0]; | |
if(item.PlanEstimate) { | |
dojo.create('span', {innerHTML: " (" + item.PlanEstimate + ")"}, header); | |
} | |
dojo.query('a', header).connect("onmouseover", this._linkMouseOver); | |
if (projectOidsInScope.indexOf(',') > -1) { | |
dojo.create('div',{innerHTML:'<b>Project:</b> ' + item.Project._refObjectName},cardContent); | |
} | |
var currentRelease = releaseDropdown.getSelectedItem()._ref; | |
var itemRelease = item.Release && item.Release._ref; | |
if(currentRelease != itemRelease && item.Iteration !== null){ | |
dojo.create('div', {innerHTML: 'Note: Not in the selected Release', style: {color:'#666666', fontSize: '10px'}}, cardContent); | |
} | |
return card; | |
}; | |
/* override so you can't mark a card as Ready */ | |
this._addReadyDivToMenu = function(){}; | |
}; | |
/** | |
Copyright (c) 2011 Rally Software Development Corp. All rights reserved | |
*/ | |
var PlanningBoardColumnRenderer = function(board, value, options) { | |
rally.sdk.ui.cardboard.BasicColumnRenderer.call(this, board, value, options); | |
var that = this; | |
var dndContainer; | |
var points = 0; | |
var columnDiv; | |
var pointCapacity = Infinity; | |
var resourcesDisplay; | |
var currentIterationDisplay; | |
this.render = function() { | |
columnDiv = document.createElement("div"); | |
dojo.addClass(columnDiv, "column"); | |
var columnHeader = document.createElement("div"); | |
dojo.addClass(columnHeader, "columnHeader"); | |
var columnHeaderText = document.createElement("span"); | |
columnHeader.appendChild(columnHeaderText); | |
columnHeaderText.appendChild(document.createTextNode(options.displayValue)); | |
var extraInformationContainer = dojo.create('div', {}, columnHeader); | |
dojo.addClass(extraInformationContainer, 'extraInformationContainer'); | |
resourcesDisplay = dojo.create('span', {}, extraInformationContainer); | |
currentIterationDisplay = dojo.create('span', {}, extraInformationContainer); | |
setCapacityText(); | |
if(options.iteration){ | |
var editLink = new rally.sdk.ui.basic.EditLink({ | |
item: options.iteration._ref | |
}); | |
var linkContainer = dojo.create('div', {style: {fontSize: '10px', fontWeight: 'normal'}}, columnHeader); | |
editLink.display(linkContainer); | |
} | |
columnDiv.appendChild(columnHeader); | |
if (options.startDate) { | |
var tooltip = new rally.sdk.ui.basic.Tooltip({ | |
message: rally.sdk.util.DateTime.format(options.startDate, "MM/dd/yyyy") + " - " + | |
rally.sdk.util.DateTime.format(options.endDate, "MM/dd/yyyy"), | |
position: "above" | |
}); | |
tooltip.display(columnHeaderText); | |
} | |
dndContainer = document.createElement("div"); | |
dojo.addClass(dndContainer, "columnContent"); | |
columnDiv.appendChild(dndContainer); | |
return columnDiv; | |
}; | |
this.getDndContainer = function() { | |
return dndContainer; | |
}; | |
this.getColumnNode = function() { | |
return columnDiv; | |
}; | |
function setCapacityText() { | |
pointCapacity = options.resources; | |
if (options && options.resources) { | |
resourcesDisplay.innerHTML = getCapacityText(points, pointCapacity); | |
} | |
if(options && options.startDate && | |
rally.sdk.util.DateTime.fromIsoString(options.startDate) < new Date() && | |
options.endDate && rally.sdk.util.DateTime.fromIsoString(options.endDate) > new Date()) { | |
currentIterationDisplay.innerHTML = " *Current"; | |
} | |
} | |
function getCapacityText(points, pointCapacity) { | |
var pointDisplay = parseInt(points, 10) === parseFloat(points) ? points : Number(points).toFixed(2); | |
return "(" + pointDisplay + "/" + (pointCapacity === Infinity ? | |
'<span class="infinitySymbol">∞</span>)' : (pointCapacity + ")")); | |
} | |
this.addNoDropClass = function(nodes) { | |
if (pointCapacity === Infinity) { | |
return false; | |
} | |
return pointCapacity <= points; | |
}; | |
this.cardRemoved = function(card) { | |
points = points - card.PlanEstimate; | |
setCapacityText(); | |
if (pointCapacity >= points) { | |
dojo.removeClass(columnDiv, "overCapacity"); | |
} | |
}; | |
this.cardAdded = function(card) { | |
points = points + card.PlanEstimate; | |
setCapacityText(); | |
if (pointCapacity < points) { | |
dojo.addClass(columnDiv, "overCapacity"); | |
} | |
}; | |
this.getDndContainer = function() { | |
return dndContainer; | |
}; | |
}; | |
var releaseDropdown; | |
function PlanningBoard() { | |
var checkBoxes; | |
var columnStates; | |
var cardboard; | |
var rallyDataSource; | |
var projectIterationMap = {}; | |
var MAX_COLUMNS = 12; | |
var MAX_CARDS = Infinity; | |
var that = this; | |
var fetchParams = "Name,FormattedID,Owner,ObjectID,Rank,PlanEstimate,Iteration,Blocked,Release"; | |
function addPageTools(){ | |
var header = rally.sdk.ui.AppHeader; | |
header.addPageTool({ | |
key:"newUserStory", | |
label: "New User Story...", | |
onClick: function() { | |
rally.sdk.util.Navigation.popupCreatePage("hierarchicalrequirement"); | |
} | |
}); | |
header.addPageTool({ | |
key:"newIteration", | |
label: "New Iteration...", | |
onClick: function() { | |
rally.sdk.util.Navigation.popupCreatePage("iteration"); | |
} | |
}); | |
header.addPageTool({ | |
key:"newRelease", | |
label: "New Release...", | |
onClick: function() { | |
rally.sdk.util.Navigation.popupCreatePage("release"); | |
} | |
}); | |
} | |
addPageTools(); | |
this._createLayout = function(element) { | |
var headerDiv = document.createElement("div"); | |
element.appendChild(headerDiv); | |
var dropdownContainerDiv = document.createElement("div"); | |
dojo.addClass(dropdownContainerDiv, "dropdownContainer"); | |
headerDiv.appendChild(dropdownContainerDiv); | |
var dropdownDiv = document.createElement("span"); | |
dropdownDiv.id = "dropdown"; | |
dropdownContainerDiv.appendChild(dropdownDiv); | |
var checkBoxContainerDiv = document.createElement("div"); | |
dojo.addClass(checkBoxContainerDiv, "typeFilterContainer"); | |
headerDiv.appendChild(checkBoxContainerDiv); | |
var showSpan = document.createElement("span"); | |
showSpan.appendChild(document.createTextNode("Show:")); | |
checkBoxContainerDiv.appendChild(showSpan); | |
var userStoriesSpan = document.createElement("span"); | |
userStoriesSpan.id = "userStories"; | |
checkBoxContainerDiv.appendChild(userStoriesSpan); | |
var defectsSpan = document.createElement("span"); | |
defectsSpan.id = "defects"; | |
checkBoxContainerDiv.appendChild(defectsSpan); | |
var defectSuitesSpan = document.createElement("span"); | |
defectSuitesSpan.id = "defectSuites"; | |
checkBoxContainerDiv.appendChild(defectSuitesSpan); | |
//Create checkboxes | |
checkBoxes = []; | |
var userStoriesCheckBox = new rally.sdk.ui.basic.CheckBox({ | |
showLabel: true, | |
label: "User Stories", | |
labelPosition: "after", | |
value: "HierarchicalRequirement", | |
checked: true | |
}); | |
checkBoxes.push(userStoriesCheckBox); | |
userStoriesCheckBox.display("userStories"); | |
var defectsCheckBox = new rally.sdk.ui.basic.CheckBox({ | |
showLabel: true, | |
label: "Defects", | |
labelPosition: "after", | |
value: "Defect" | |
}); | |
checkBoxes.push(defectsCheckBox); | |
defectsCheckBox.display("defects"); | |
var defectSuitesCheckBox = new rally.sdk.ui.basic.CheckBox({ | |
showLabel: true, | |
label: "Defect Suites", | |
labelPosition: "after", | |
value: "DefectSuite" | |
}); | |
checkBoxes.push(defectSuitesCheckBox); | |
defectSuitesCheckBox.display("defectSuites"); | |
//Wire up events | |
dojo.forEach(checkBoxes, function(checkBox) { | |
checkBox.addEventListener("onChange", that._refreshBoard); | |
}); | |
var planningBoardDiv = document.createElement("div"); | |
planningBoardDiv.id = "planningBoard"; | |
dojo.addClass(planningBoardDiv, "planningBoard"); | |
element.appendChild(planningBoardDiv); | |
}; | |
this._getSelectedTypes = function() { | |
var types = []; | |
//Build types based on checkbox selections | |
dojo.forEach(checkBoxes, function(checkBox) { | |
if (checkBox.getChecked()) { | |
types.push(checkBox.getValue()); | |
} | |
}); | |
return types; | |
}; | |
this._getItems = function(callback) { | |
var queries = []; | |
var backlogQueries = []; | |
var releaseBacklogQueries = []; | |
dojo.forEach(['HierarchicalRequirement','Defect','DefectSuite'], function(type) { | |
queries.push({ | |
key:type, | |
type: type, | |
fetch: fetchParams, | |
query: rally.sdk.util.Query.and(['Iteration.StartDate >= "' + releaseDropdown.getSelectedStart() + '"', | |
'Iteration.StartDate <= "' + releaseDropdown.getSelectedEnd() + '"']), | |
order: "Rank" | |
}); | |
releaseBacklogQueries.push({ | |
key:type + "relBacklog", | |
type: type, | |
fetch: fetchParams + ',Children', | |
query: new rally.sdk.util.Query('Iteration = "null"').and(releaseDropdown.getQueryFromSelected()), | |
order: "Rank" | |
}); | |
}); | |
dojo.forEach(that._getSelectedTypes(), function(type) { | |
backlogQueries.push({ | |
key:type + "backlog", | |
type: type, | |
fetch: fetchParams + ',Children', | |
query: new rally.sdk.util.Query('Iteration = "null"').and('Release = "null"'), | |
order: "Rank" | |
}); | |
}); | |
var items = []; | |
var outstandingQueries = 3; | |
function concatResults(queryArray, results) { | |
rally.forEach(queryArray, function(query) { | |
if (results[query.key]) { | |
items = items.concat(results[query.key]); | |
} | |
}); | |
outstandingQueries--; | |
if(outstandingQueries === 0) { | |
rally.forEach(items, function(item) { | |
if(item.Iteration !== null){ | |
item._iterationName = item.Iteration.Name; | |
} else if(item.Release !== null){ | |
item._iterationName = 'releaseBacklog'; | |
} else { | |
item._iterationName = 'backlog'; | |
} | |
}); | |
//Filter out epics | |
items = dojo.filter(items, function(item) { | |
return !(item.Children && item.Children.length > 0); | |
}); | |
callback(items); | |
} | |
} | |
rallyDataSource.find(releaseBacklogQueries, function(results) {concatResults(releaseBacklogQueries, results); }); | |
rallyDataSource.find(backlogQueries, function(results) { concatResults(backlogQueries, results); }); | |
rallyDataSource.findAll(queries, function(results) { concatResults(queries, results); }); | |
}; | |
this._getColumns = function(columnCallback) { | |
columnStates = {}; | |
projectIterationMap = {}; | |
var releaseStart = releaseDropdown.getSelectedStart(); | |
var releaseEnd = releaseDropdown.getSelectedEnd(); | |
var query = rally.sdk.util.Query.and([ | |
'StartDate >= "' + releaseStart + '"', | |
'StartDate <= "' + releaseEnd + '"' | |
]) | |
.or(rally.sdk.util.Query.and([ | |
'EndDate >= "' + releaseStart + '"', | |
'EndDate <= "' + releaseEnd + '"' | |
])) | |
.or(rally.sdk.util.Query.and([ | |
'StartDate <= "' + releaseStart + '"', | |
'EndDate >= "' + releaseEnd + '"' | |
])); | |
var iterationQuery = { | |
key:"iterations", | |
type: "Iteration", | |
fetch: "Name,Project,StartDate,EndDate,Resources", | |
order: "EndDate", | |
query: query | |
}; | |
rallyDataSource.findAll(iterationQuery, function(results) { | |
columnStates = { | |
"backlog":{ | |
displayValue: "Backlog", | |
resources:Infinity | |
}, | |
"releaseBacklog": { | |
displayValue: "Release Backlog", | |
resources: Infinity | |
} | |
}; | |
dojo.forEach(results.iterations, function(iteration) { | |
// Create a map between project and the iterations its contains by iteration name. | |
var projectRef = rally.sdk.util.Ref.getRelativeRef(iteration.Project); | |
projectIterationMap[projectRef] = projectIterationMap[projectRef] || {}; | |
projectIterationMap[projectRef][iteration.Name] = rally.sdk.util.Ref.getRelativeRef(iteration); | |
// Builds list of unique iteration names and rolls up capacity | |
if (columnStates[iteration.Name]) { | |
columnStates[iteration.Name].resources += iteration.Resources; | |
} | |
else { | |
columnStates[iteration.Name] = {displayValue:iteration.Name, | |
resources:iteration.Resources || 0, | |
startDate: iteration.StartDate, | |
endDate: iteration.EndDate | |
}; | |
} | |
//only show the edit link of the iteration if one project in scope | |
//otherwise we might let them edit the wrong iteration and confuse them | |
//also don't show if we're external | |
if(!isNaN(projectOidsInScope)){ | |
columnStates[iteration.Name].iteration = iteration; | |
} | |
}); | |
//remove columns older than today, except for the last one | |
var newestOldEndDate = 0; | |
var newestOldIterationName; | |
var now = new Date(); | |
rally.forEach(columnStates, function(iteration, iterationName){ | |
//backlog and release backlog won't have an end date | |
if(!iteration.endDate){ | |
return; | |
} | |
var endDate = rally.sdk.util.DateTime.fromIsoString( iteration.endDate); | |
if(endDate < now){ | |
if(endDate > newestOldEndDate){ | |
newestOldEndDate = endDate; | |
newestOldIterationName = iterationName; | |
} | |
columnStates[iterationName].forDeletion = true; | |
} | |
}); | |
if(newestOldEndDate !== 0){ | |
columnStates[newestOldIterationName].forDeletion = false; | |
} | |
rally.forEach(columnStates, function(iteration, iterationName){ | |
if(iteration.forDeletion === true){ | |
delete columnStates[iterationName]; | |
} | |
}); | |
columnCallback(columnStates); | |
}); | |
}; | |
this._postProcessColumnChange = function(obj, args) { | |
var projectRef = rally.sdk.util.Ref.getRelativeRef(args.originalItem.Project); | |
var iterationName = args.fieldsToUpdate[args.attribute]; | |
if(iterationName === undefined){ | |
delete args.fieldsToUpdate[args.attribute]; | |
return; | |
} | |
if (iterationName === "backlog") { | |
args.fieldsToUpdate.Iteration = "null"; | |
args.fieldsToUpdate.Release = "null"; | |
} | |
else if (iterationName == "releaseBacklog") { | |
args.fieldsToUpdate.Release = releaseDropdown.getSelectedItem(); | |
args.fieldsToUpdate.Iteration = "null"; | |
} | |
else if (projectIterationMap[projectRef][iterationName]) { | |
args.fieldsToUpdate.Iteration = projectIterationMap[projectRef][iterationName]; | |
} | |
else { | |
rally.forEach(projectIterationMap, function(value, key) { | |
if (value[iterationName]) { | |
args.fieldsToUpdate.Iteration = value[iterationName]; | |
} | |
}); | |
} | |
delete args.fieldsToUpdate[args.attribute]; | |
}; | |
this._refreshBoard = function() { | |
var cardboardConfig = { | |
types: [], | |
attribute: "_iterationName", | |
sortAscending: true, | |
order: "Rank", | |
columns:that._getColumns, | |
columnRenderer:PlanningBoardColumnRenderer, | |
cardRenderer:PlanningBoardCardRenderer, | |
maxColumnsPerBoard: MAX_COLUMNS, | |
maxCardsPerColumn: MAX_CARDS, | |
items:that._getItems, | |
fetch: fetchParams | |
}; | |
cardboardConfig.types = that._getSelectedTypes(); | |
if (cardboard) { | |
cardboard.destroy(); | |
} | |
cardboard = new rally.sdk.ui.CardBoard(cardboardConfig, rallyDataSource); | |
cardboard.addEventListener(cardboard.getValidEvents().preUpdate, that._postProcessColumnChange); | |
cardboard.display("planningBoard"); | |
}; | |
this.display = function(element) { | |
rally.sdk.ui.AppHeader.showPageTools(true); | |
//Build app layout | |
this._createLayout(element); | |
rallyDataSource = new rally.sdk.data.RallyDataSource('__WORKSPACE_OID__', | |
'__PROJECT_OID__', | |
'__PROJECT_SCOPING_UP__', | |
'__PROJECT_SCOPING_DOWN__'); | |
rallyDataSource.setServer('ppmtest.rallydev.com'); | |
PlanningBoardCardRenderer.rallyDataSource = rallyDataSource; | |
var dropdownConfig = { | |
showLabel: true, query: '(State != "Accepted")' | |
}; | |
releaseDropdown = new rally.sdk.ui.ReleaseDropdown(dropdownConfig, rallyDataSource); | |
releaseDropdown.addEventListener("onChange", that._refreshBoard); | |
releaseDropdown.addEventListener("onLoad", that._refreshBoard); | |
releaseDropdown.display("dropdown"); | |
}; | |
} | |
</script> | |
<style type="text/css"> | |
.dropdownContainer { | |
float: left; | |
} | |
.typeFilterContainer { | |
float: right; | |
margin-right: 30px; | |
} | |
.planningEstimate{ | |
display:inline; | |
} | |
.planningBoard { | |
clear: both; | |
} | |
.overCapacity .columnHeader { | |
color: #FC5350; | |
} | |
.overCapacity .columnContent { | |
background-color: #F9E2E1; | |
} | |
.cardboard .columnHeader { | |
height: 50px; | |
} | |
.cardboard .columnHeader.current { | |
color: #000000; | |
font-weight:bold; | |
} | |
.cardboard .extraInformationContainer { | |
font-size: 12px; | |
line-height: 20px; | |
} | |
.cardboard .infinitySymbol { | |
font-size: 16px; | |
position: relative; | |
top: 2px; | |
} | |
.dijitTooltip { | |
width: 200px; | |
} | |
</style> | |
<script type="text/javascript"> | |
projectOidsInScope = '__PROJECT_OIDS_IN_SCOPE__'; | |
function onLoad() { | |
var planningBoard = new PlanningBoard(); | |
planningBoard.display(dojo.body()); | |
} | |
rally.addOnLoad(onLoad); | |
</script> | |
</head> | |
<body> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment