<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>Task Board</title> <link type="text/css" rel="stylesheet" href="__SERVER_URL__/js-lib/yui/2.6.0/build/reset-fonts-grids/reset-fonts-grids.css"/> <link type="text/css" rel="stylesheet" href="__SERVER_URL__/css/toolkit/0.02/renderer.css"/> <link type="text/css" rel="stylesheet" href="__SERVER_URL__/css/toolkit/0.02/grid.css"/> <link type="text/css" rel="stylesheet" href="__SERVER_URL__/css/toolkit/0.02/editor.css"/> <link type="text/css" rel="stylesheet" href="__SERVER_URL__/css/toolkit/0.02/toolkit.css"/> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/yui.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/toolkit.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/date.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/modal.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/html.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/connection.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/grid.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/renderer/renderer.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/editor.js"></script> <script type="text/javascript" src="__SERVER_URL__/js/toolkit/0.02/datasource.js"></script> <script type="text/javascript"> function initTaskboard() { RALLY.toolkit.HTML.createMaskLayer(); var statusDiv = null; var setStatus = function(msg) { if (!statusDiv) { statusDiv = Dom.get('status'); } if (msg) { RALLY.toolkit.HTML.enableMask(null, 0); Dom.addClass(window.document.body, 'show-progress'); } else { RALLY.toolkit.HTML.disableMask(); Dom.removeClass(window.document.body, 'show-progress'); } statusDiv.innerHTML = msg || ''; } var insideRally = RALLY.toolkit.insideRally(); var showActuals = false; var currentProjectOid = '__PROJECT_OID__'; var projectScopeUp = '__PROJECT_SCOPING_UP__' == 'true'; var projectScopeDown = '__PROJECT_SCOPING_DOWN__' == 'true'; // use this to rebuild the full query, stitching in the // project scoping information from the top-level variables above function getQuery() { var scoping = "&project=${currentProject}&projectScopeUp=" + projectScopeUp + "&projectScopeDown=" + projectScopeDown; var paging = "&pagesize=100"; return { "currentProject" : "/iteration:current/project", "#storyType" : "/typedefinition?query=(Name = \"Hierarchical Requirement\")", "taskUnit" : "${iteration/workspace/workspaceConfiguration/taskUnitName}", "storyStates" : "${#storyType/attributes[name=schedule state]/allowedvalues/stringvalue}", "iteration" : "/iteration:current?fetch=name,objectid&order=StartDate", "iterations" : "/iterations?fetch=name,objectid&order=StartDate,Name&project=${currentProject}&projectScopeUp=false&projectScopeDown=false" + paging, "users" : "/users?fetch=displayname,loginname,emailaddress,objectid" + paging, "tasks" : "/tasks?fetch=taskindex,name,objectid,formattedid,owner,blocked,estimate,todo,actuals,state,workproduct&query=(Iteration = ${iteration})" + scoping + paging, "stories" : "/hierarchicalrequirement?fetch=rank,blocked,formattedid,name,objectid,owner,project,schedulestate,taskestimatetotal,taskremainingtotal,taskactualtotal,tasks&order=Rank&query=(Iteration = ${iteration})" + scoping + paging, "defects" : "/defect?fetch=rank,blocked,formattedid,name,objectid,owner,project,schedulestate,taskestimatetotal,taskremainingtotal,taskactualtotal&order=Rank&query=(Iteration = ${iteration})" + scoping + paging, "defectsuite" : "/defectsuite?fetch=rank,blocked,formattedid,name,objectid,owner,project,schedulestate,taskestimatetotal,taskremainingtotal,taskactualtotal&order=Rank&query=(Iteration = ${iteration})" + scoping + paging, "testsets" : "/testset?fetch=rank,blocked,formattedid,name,objectid,owner,project,schedulestate,taskestimatetotal,taskremainingtotal,taskactualtotal&query=(Iteration = ${iteration})" + scoping + paging }; } var query = getQuery(); var renderTimeCell = function(label, value, c) { var v = (Lang.isValue(value)) ? value : '-'; return '<div class="' + c + '"><div>' + label + '</div><span>' + v + '</span></div>'; }; var projScopeUpControl = Dom.get('proj_scope_up_control'); var projScopeDownControl = Dom.get('proj_scope_down_control'); if (insideRally) { Dom.setStyle(Dom.get('project'), 'display', 'none'); } else { projScopeUpControl.checked = projectScopeUp; projScopeDownControl.checked = projectScopeDown; var updateAfterScopeChange = function(e) { var src = Event.getTarget(e); if (src.id == 'proj_scope_up_control') { projectScopeUp = src.checked; } else if (src.id == 'proj_scope_down_control') { projectScopeDown = src.checked; } query = getQuery(); RALLY.toolkit.showMessage('Refreshing iteration with selected project scoping'); gridController.display(); }; Event.addListener(projScopeUpControl, 'click', updateAfterScopeChange); Event.addListener(projScopeDownControl, 'click', updateAfterScopeChange); } var viewConfig = { // configure columns columnAttribute: "State", columnValuesAccessor: function(model, modelSchema) { var stateOptions = modelSchema.Task.State.options; var stateNames = []; for (var i = 0, length = stateOptions.length; i < length; i++) { stateNames.push(stateOptions[i].Value); } return stateNames; }, columnHeaderRenderer: function(container, value) { container.innerHTML = value; }, // configure rows rowAttribute: "WorkProduct", rowKeyAccessor: function(workProduct) { return workProduct.ObjectID; }, rowValuesAccessor: function(model, modelSchema) { return model.items[0].workProducts; }, rowHeaderRenderer: function(container, value, modelSchema) { var state, html = [], schema = modelSchema.WorkProduct, owner, ownerClass = ['owner'], divId = 'rally-workprod-' + value.ObjectID, divClass = ['rally-workprod'], timeClass = ['rally-time']; // owner could be an object literal, or a string (if the user has been deleted in the app) if (!value.Owner) { ownerClass.push('de-emphasis'); owner = gridController.noOwnerLabel; } else if (Lang.isObject(value.Owner)) { owner = value.Owner._refObjectName; } else { ownerClass.push('de-emphasis deleted-owner'); owner = value.Owner; } if (value.ScheduleState == 'Accepted') { divClass.push('rally-workprod-accepted'); divClass.push('de-emphasis'); } else if (value.TaskRemainingTotal == '' || value.TaskRemainingTotal == 0) { timeClass.push('de-emphasis'); } html.push('<div class="' + divClass.join(' ') + '" id="' + divId + '">'); state = new RALLY.toolkit.renderer.StateRenderer({ schema: schema.State, state: value.ScheduleState, blocked: value.Blocked }); html.push('<div class="state">' + state.display(false, value.ScheduleState == 'Accepted') + '</div>'); html.push('<div class="id">' + value.FormattedID + '</div>'); html.push('<div class="name">' + RALLY.toolkit.niceSubstring(value.Name, 100) + '</div>'); html.push('<div class="' + ownerClass.join(' ') + '">' + owner + '</div>'); html.push(renderTimeCell((schema.TaskEstimateTotal) ? schema.TaskEstimateTotal.DisplayName : 'Est', value.TaskEstimateTotal, timeClass.join(' '))); html.push(renderTimeCell((schema.TaskRemainingTotal) ? schema.TaskRemainingTotal.DisplayName : 'To Do', value.TaskRemainingTotal, timeClass.join(' '))); if (showActuals) { html.push(renderTimeCell((schema.TaskActualTotal) ? schema.TaskActualTotal.DisplayName : 'Actuals', value.TaskActualTotal, timeClass.join(' '))); } html.push('<div class="clear"></div>'); html.push('</div>'); container.innerHTML = html.join(''); }, // configure cells cellSortFunction: function(a, b) { return a - b; }, // configure items itemRankAccessor: function(item) { return item.TaskIndex }, itemKeyAccessor: function(item) { return item.ObjectID }, itemsAccessor: function(model) { return model.items[0].tasks; }, itemRenderer: function(container, item, modelSchema) { var html = [], owner, taskClass = ['rally-task'], ownerClass = ['owner'], estClass = ['rally-time'], todoClass = ['rally-time'], actClass = ['rally-time'], schema = modelSchema.Task, contentStyle = '', editIconId = 'edit-' + Dom.generateId(), deleteIconId = 'delete-' + Dom.generateId(); // owner could be an object literal, or a string (if the user has been deleted in the app) if (!item.Owner) { ownerClass.push('de-emphasis'); owner = gridController.noOwnerLabel; } else if (Lang.isObject(item.Owner)) { owner = item.Owner._refObjectName; } else { ownerClass.push('de-emphasis deleted-owner'); owner = item.Owner; } if (item.Blocked) { taskClass.push('rally-task-blocked'); } if (item.State == 'Defined') { todoClass.push('de-emphasis'); actClass.push('de-emphasis'); } else if (item.State == 'Completed') { todoClass.push('de-emphasis'); } if (item.WorkProduct && item.WorkProduct.ScheduleState == 'Accepted') { taskClass.push('rally-task-accepted'); taskClass.push('de-emphasis'); } html.push('<div class="' + taskClass.join(' ') + '" id="rally-task-' + item.ObjectID + '">'); html.push('<div class="actions">'); html.push('<img id="' + editIconId + '" src="' + RALLY.toolkit.Connection.getServerURL() + '/images/icon_pencil.gif" alt="Edit" />'); html.push('<img id="' + deleteIconId + '" src="' + RALLY.toolkit.Connection.getServerURL() + '/images/icon_delete.gif" alt="Delete" />'); html.push('</div>'); if (item.Owner && Lang.isValue(item.Owner.ObjectID)) { // if there is an image, we need to pad the content to account for it contentStyle = 'margin-left: 70px'; html.push('<div class="image">'); html.push('<img id="edit-control" src="' + RALLY.toolkit.Connection.getServerURL() + '/profile/viewThumbnailImage.sp?tSize=60&uid=' + item.Owner.ObjectID + '" alt="" />'); html.push('</div>'); } html.push('<div style="' + contentStyle + '">'); html.push('<div class="id">' + item.FormattedID + '</div>'); html.push('<div class="name">' + RALLY.toolkit.niceSubstring(item.Name) + '</div>'); html.push('<div class="' + ownerClass.join(' ') + '">' + owner + '</div>'); html.push('<div class="time">'); html.push(renderTimeCell((schema.Estimate) ? schema.Estimate.ShortName : 'Est', item.Estimate, estClass.join(' '))); html.push(renderTimeCell((schema.ToDo) ? schema.ToDo.ShortName : 'To Do', item.ToDo, todoClass.join(' '))); if (showActuals) { html.push(renderTimeCell((schema.Actuals) ? schema.Actuals.ShortName : 'Actuals', item.Actuals, actClass.join(' '))); } html.push('<div class="clear"></div>'); html.push('</div>'); html.push('</div>'); if (item.Blocked) { html.push('<img class="blocked-icon" src="' + RALLY.toolkit.Connection.getServerURL() + '/images/icon_blocked.gif" alt="Blocked" />'); html.push('<div class="clear"></div>'); } html.push('</div>'); container.innerHTML = html.join(''); Event.purgeElement(editIconId); Event.addListener(editIconId, 'click', function(e) { gridController.showEditor(item, schema); }); Event.purgeElement(deleteIconId); Event.addListener(deleteIconId, 'click', function(e) { gridController.deleteItem(item); }); }, // configure drag event dragDropCallback: function(item, value) { setStatus('Saving changes...'); var cell = this.getRenderedItem(item); if (cell) { var anim = new YAHOO.util.ColorAnim(cell, { backgroundColor: { from: '#F5F4CD', to: '#fff' } }); anim.animate(); } gridController.saveChanges(item, { 'State': value }, 'taskboard', function() { setStatus(); }); } }; RALLY.toolkit.Controller = function(query, viewConfig) { this.query = query; this.schemaConfig = {}; this.viewConfig = viewConfig; this.dataSource = new RALLY.toolkit.TaskboardDataSource(query, '__SERVER_URL__'); this.view = new RALLY.toolkit.Grid('taskboard', this.viewConfig); this.editor = new RALLY.toolkit.Editor(); }; RALLY.toolkit.Controller.prototype = { acceptedCookieKey: 'taskboard-hide-accepted', ownerCookieKey: 'taskboard-filter-by-owner', iterationCookieKey: 'taskboard-filter-by-iteration', projectOidCookieKey:'taskboard-current-project', noOwnerLabel: 'No Owner', allOwnersLabel: 'All Team Members', getIterationByOid: function(objectID) { for (var i = 0; i < this.iterations.length; i++) { if (this.iterations[i].ObjectID == objectID) { return this.iterations[i]; } } return null; }, display: function(iterationOid) { var len, iterations, selectedIteration, html = [], that = this, itr = (iterationOid) ? iterationOid : RALLY.toolkit.Cookie.get(this.iterationCookieKey) || '', projectOid = RALLY.toolkit.Cookie.get(this.projectOidCookieKey), curProjectOid = (currentProjectOid != '') ? currentProjectOid : null; // if our selcted project has changed, reset the iteration ObjectID in the cookie if (projectOid != curProjectOid) { itr = ''; RALLY.toolkit.Cookie.remove(this.iterationCookieKey); } RALLY.toolkit.Cookie.add(this.projectOidCookieKey, curProjectOid); // re-write the query to scope to the specified iteration if (itr) { query.iteration = "/iterations?fetch=name,objectid&order=StartDate&query=(ObjectID = " + itr + ")"; } else { query.iteration = "/iteration:current?fetch=name,objectid&order=StartDate"; } setStatus('Loading...'); if (this.dataSource) { var queryWithParams = { adHocQuery: query, cpoid: currentProjectOid }; this.dataSource.get(function(model) { var i; that.hideAcceptedControl = Dom.get('hide_accepted_control'); that.iterationSelect = Dom.get('change_iteration_control'); // clear out the grid container and any state-dependant vars YAHOO.util.Dom.get('taskboard').innerHTML = ''; that.users = []; YAHOO.util.Event.removeListener(that.hideAcceptedControl, 'click'); YAHOO.util.Event.removeListener(that.iterationSelect, 'change'); if (model.errors.length > 0) { html.push('<ul>'); for (i = 0,len = model.errors.length; i < len; i++) { html.push('<li>' + model.errors[i].message + '</li>'); } html.push('</ul>'); RALLY.toolkit.showError(html.join('')); setStatus(); return; } else if (model.items.length == 0 || (model.items.length == 1 && model.items[0].iteration == null)) { RALLY.toolkit.showError('There are no stories to display in the selected project'); setStatus(); return; } // add in the header info Dom.get('proj_name').innerHTML = model.items[0].project.Name; Dom.get('info').innerHTML = (model.items[0].iteration.Name || '') + '<br/>' + (RALLY.Date.formatNow()); // build the iteration select selectedIteration = RALLY.toolkit.Cookie.get(that.iterationCookieKey); iterations = model.items[0].iterations; that.iterations = model.items[0].iterations; RALLY.toolkit.HTML.clearSelect(that.iterationSelect); for (i = 0,len = iterations.length; i < len; i++) { that.iterationSelect.options[that.iterationSelect.options.length] = new Option(iterations[i].Name, iterations[i].ObjectID); if (iterations[i].ObjectID == selectedIteration || iterations[i].Name == model.items[0].iteration.Name) { RALLY.toolkit.Cookie.add(that.iterationCookieKey, iterations[i].ObjectID); that.iterationSelect.selectedIndex = i; } } Event.removeListener(that.iterationSelect, 'change'); Event.addListener(that.iterationSelect, 'change', that.updateIteration, that, true); if (model.items[0].tasks.length == 0 && model.items[0].workProducts.length == 0) { RALLY.toolkit.showError('There are no stories to display for the given iteration'); setStatus(); return; } // build the grid that.view.display(model); // should we hide accepted work? if (RALLY.toolkit.Cookie.get(that.acceptedCookieKey) == 'true' || RALLY.toolkit.Cookie.get(that.acceptedCookieKey) == null) { that.hideAcceptedControl.checked = true; that.toggleAccepted(); } Event.removeListener(that.hideAcceptedControl, 'click'); Event.addListener(that.hideAcceptedControl, 'click', that.toggleAccepted, that, true); // add users into the select that were found during the rendering that.refreshUserSelect(); setStatus(); }, queryWithParams); } }, showEditor: function(item, schema) { var editorConfig = { props: (showActuals) ? ['Estimate', 'ToDo', 'Actuals', 'Owner', 'State'] : ['Estimate', 'ToDo', 'Owner', 'State'], propertyKeyAccessors: { Owner: function(prop) { return prop != null ? prop.LoginName : null; } }, propertyValueAccessors: { Owner: function(prop) { return prop != null ? prop._refObjectName : null; } }, titleAccessor: function(obj) { return obj.FormattedID + '<br/>' + obj.Name; }, onErrorCallback: function() { gridController.display(); }, onSaveCallback: function(changes) { gridController.saveChanges(item, changes, 'editor'); } }; this.editor.display(item, schema, editorConfig); }, getParentRow: function(el) { while (el && el.tagName && el.tagName.toUpperCase() != 'TR') { el = el.parentNode; } return el; }, toggleAccepted: function() { var i, len, row, hideAcceptedControl = this.hideAcceptedControl || Dom.get('hide_accepted_control'), userSelect = this.userSelect || Dom.get('filter_user_control'), hideAccepted = hideAcceptedControl.checked, workProds = Dom.getElementsByClassName('rally-workprod-accepted'); RALLY.toolkit.Cookie.add(this.acceptedCookieKey, hideAccepted); for (i = 0,len = workProds.length; i < len; i++) { row = this.getParentRow(workProds[i]); if (hideAccepted) { Dom.addClass(row, 'accepted'); } else { Dom.removeClass(row, 'accepted'); } } if (hideAccepted && this.numItemsVisible() == 0) { if (userSelect.selectedIndex >= 0 && userSelect.options[userSelect.selectedIndex].value == this.allOwnersLabel) { RALLY.toolkit.showWarning('The selected iteration contains only accepted stories'); } else { RALLY.toolkit.showWarning('The selected owner has only accepted stories and/or tasks'); } } }, refreshUserSelect: function() { this.userSelect = this.userSelect || Dom.get('filter_user_control'); var selectedOwner = 0, selectedIndex = -1, options, defaultUsers = [ this.allOwnersLabel, this.noOwnerLabel ], users = this.getOwnersOnTaskboard(); if (this.userSelect.selectedIndex >= 0) { selectedOwner = this.userSelect.options[this.userSelect.selectedIndex].value; } else if (RALLY.toolkit.Cookie.get(this.ownerCookieKey)) { selectedOwner = RALLY.toolkit.Cookie.get(this.ownerCookieKey); } // clear out current options RALLY.toolkit.HTML.clearSelect(this.userSelect); users.sort(function(a, b) { return (a.toLowerCase() < b.toLowerCase()) ? -1 : 1; }); options = defaultUsers.concat(users); for (i = 0,len = options.length; i < len; i++) { this.userSelect.options[this.userSelect.options.length] = new Option(options[i], options[i]); if (selectedOwner == options[i]) { selectedIndex = i; } } if (selectedIndex == -1) { // if we tried to find an owner that didn't exist, default to 'All' selectedIndex = 0; if (selectedOwner) { RALLY.toolkit.showWarning('The selected owner \'' + selectedOwner + '\' does not exist in this view. Reset the filtering to ' + this.allOwnersLabel); } } this.userSelect.selectedIndex = '' + selectedIndex; YAHOO.util.Event.removeListener(this.userSelect, 'change') YAHOO.util.Event.addListener(this.userSelect, 'change', this.filterByOwner, this, true); RALLY.toolkit.Cookie.add(this.ownerCookieKey, selectedOwner); this.filterByOwner(); }, saveChanges: function(item, changes, source, successCallback) { var that = this, i, len, html = [], error, isConcurrencyError; if (!Lang.isValue(source)) { source = 'taskboard'; } // set to-do to 0 if (changes.State == "Completed") { changes.ToDo = "0"; } // always include _objectVersion if (!changes._objectVersion) { changes._objectVersion = item._objectVersion; } if (this.dataSource) { this.dataSource.update(item, changes, function(model) { if (model.OperationResult.Errors.length == 0) { that.refreshItem(item); that.editor.close(); if (successCallback) { successCallback(); } } else { html.push('<ul>'); for (i = 0,len = model.OperationResult.Errors.length; i < len; i++) { error = model.OperationResult.Errors[i]; if (error.toLowerCase().indexOf('concurrency conflict') != -1 || error.toLowerCase().indexOf('could not read') != -1) { isConcurrencyError = true; } error = that.parseSaveErrorsIntoReadableString(error); if (error) { html.push('<li>' + error + '</li>'); } } html.push('</ul>'); if (source == 'editor') { that.editor.displayError(html.join(''), isConcurrencyError); } else { RALLY.toolkit.showWarning('The task you have drag-n-dropped has been modified by another user.<br/>Refreshing task board state.'); that.display(); } } }); } }, parseSaveErrorsIntoReadableString: function(error) { var matches, tmp; // what type of error did we get back? if (error.toLowerCase().indexOf('concurrency conflict') != -1) { error = 'Item was modified by another user'; } else if (error.toLowerCase().indexOf('could not read') != -1) { error = 'Item was deleted by another user'; } else if (error.toLowerCase().indexOf('could not convert') != -1) { // let's try and strip some of the nastyness out of this message matches = error.match(/could not convert: (.*)/i); if (matches && matches.length > 1) { error = matches[1]; } error = error.replace('double', 'number'); } else if (error.toLowerCase().indexOf('validation error') != -1) { matches = error.match(/validation error: .* ([a-z]+) >= 0 is an invalid numeric/i); if (matches && matches.length > 1) { tmp = matches[1].toLowerCase(); // ignore these rollup fields if (tmp == 'taskestimatetotal' || tmp == 'taskremainingtotal') { return null; } error = '"' + tmp.substr(0, 1).toUpperCase() + tmp.substr(1) + '" must be non-negative'; } } return error; }, deleteItem: function(task) { var that = this; if (task) { this.dataSource.remove(task, function(model) { if (model.OperationResult.Errors.length == 0) { that.view.removeItem(task); if (RALLY.toolkit.insideRally() && parent && parent.RALLY) { parent.RALLY.util.showDeleteFlair({ oid: task.ObjectID, record: task, recordName : task.FormattedID + ': ' + task.Name, restorable : true }); } that.dataSource.refreshWorkProduct(task.WorkProduct, function(newWorkProduct) { that.view.refreshRowHeader(newWorkProduct); that.refreshUserSelect(); }); } else { RALLY.toolkit.showError('There was an error deleting the task'); // rebuild the task board to get back to a consistent state that.updateIteration(); } }); } }, refreshItem: function(item) { var that = this; if (item) { this.dataSource.refreshTask(item, function(model) { that.view.refreshItem(model.items[0].tasks[0]); that.refreshUserSelect(); that.view.refreshRowHeader(model.items[0].workProducts[0]); }); } }, getOwnersOnTaskboard: function() { var i, len, div, ownerMap = {}, owners = [], items = this.view.findItems(); for (i = 0,len = items.length; i < len; i++) { div = Dom.getElementsByClassName('owner', 'div', items[i])[0]; if (div && div.innerHTML != this.noOwnerLabel && !Dom.hasClass(div, 'deleted-owner')) { ownerMap[div.innerHTML] = 1; } } for (i in ownerMap) { owners.push(RALLY.toolkit.HTML.unescapeXMLEntities(i)); } return owners; }, numItemsVisible: function() { var i, len, items, num = 0; var comparator = function(el) { try { return Dom.hasClass(el, 'rally-task') || Dom.hasClass(el, 'rally-workprod'); } catch (e) { } return false; }; items = this.view.findItems(comparator); for (i = 0,len = items.length; i < len; i++) { if (!(Dom.hasClass(items[i], 'hidden') || Dom.hasClass(this.getParentRow(items[i]), 'accepted') || Dom.hasClass(this.getParentRow(items[i]), 'hidden'))) { num++; } } return num; }, filterByOwner: function() { var i, len, that = this, shown = 0, shownAccepted = 0, itemMap = {}, rows = [], items, item, sel = this.userSelect || Dom.get('filter_user_control'), selected = sel.options[sel.selectedIndex]; RALLY.toolkit.Cookie.add(this.ownerCookieKey, selected.innerHTML); var buildComparator = function(owner) { return function(item) { try { return owner.innerHTML == that.allOwnersLabel || YAHOO.util.Dom.getElementsByClassName('owner', 'div', item)[0].innerHTML == owner.innerHTML; } catch (e) { } return false; }; }; var isTask = function(o) { return Dom.hasClass(o, 'rally-task'); }; var isWorkProd = function(o) { return Dom.hasClass(o, 'rally-workprod'); }; var isParentVisible = function(el) { for (var i = 0, len = rows.length; i < len; i++) { if (rows[i] == el) { return true; } } }; items = this.view.findItems(buildComparator(selected)); for (i = 0,len = items.length; i < len; i++) { item = items[i]; if (isTask(item)) { itemMap[item.id] = 1; rows.push(this.getParentRow(item)); } else if (isWorkProd(item)) { rows.push(this.getParentRow(item)); } } items = this.view.findItems(); for (i = 0,len = items.length; i < len; i++) { item = items[i]; if (isTask(item)) { if (typeof itemMap[item.id] == 'undefined') { Dom.addClass(item, 'hidden'); } else { if (this.hideAcceptedControl.checked && Dom.hasClass(item, 'rally-task-accepted')) { shownAccepted++; } else { shown++; } Dom.removeClass(item, 'hidden'); } } else if (isWorkProd(item)) { item = this.getParentRow(item); if (isParentVisible(item)) { if (this.hideAcceptedControl.checked && Dom.hasClass(item, 'hidden')) { shownAccepted++; } else { shown++; } Dom.removeClass(item, 'hidden'); } else { Dom.addClass(item, 'hidden'); } } } if (shown == 0 && shownAccepted > 0) { RALLY.toolkit.showWarning('The selected owner has only accepted stories and/or tasks'); } if ((shown + shownAccepted) == 0) { RALLY.toolkit.showWarning('The selected owner does not own any stories or tasks'); } }, updateIteration: function() { var sel = this.iterationSelect, selected = sel.options[sel.selectedIndex]; RALLY.toolkit.Cookie.add(this.iterationCookieKey, selected.value); this.display(selected.value); } }; var config = (query.adHocQuery) ? query : { adHocQuery: query }; config.integrationInfo = {}; config.integrationInfo.Name = "Mashup: Taskboard"; config.integrationInfo.Version = "2009.5"; config.integrationInfo.Vendor = "Rally Software"; var gridController = new RALLY.toolkit.Controller(config, viewConfig); gridController.display(); } YAHOO.util.Event.addListener(window, 'load', initTaskboard); </script> <style type="text/css"> /** Customize the default grid styles **/ body { font-family: tahoma, geneva, helvetica, arial, sans-serif; } .rally-grid-item { border: 0; } .rally-grid-item-content { margin: 0; } .rally-grid-column-header { padding: .2em; } /** Custom styles for this implementation **/ #wrapper { margin: .8em; } #header { border-bottom: 2px solid #666; padding: .2em .3em; margin-bottom: .4em; } #header #title { float: left; text-align: left; } #header #title h3 { font-weight: bold; font-size: 1.6em; } #header #info { float: right; text-align: right; } .clear { clear: both; height: 0; } .hidden, .accepted { display: none; visibility: hidden; height: 0; } .de-emphasis { color: #999; } .rally-time { float: left; width: 58px; margin: .1em; padding: .1em; font-size: .8em; font-weight: normal; text-align: center; border: 1px solid #bbb; } .rally-time div { border-bottom: 1px solid #ccc; } /** work products **/ .rally-workprod { margin: .5em; font-size: .9em; font-weight: normal; } .rally-workprod .id { clear: left; } .rally-workprod .name { font-size: 1.3em; padding-bottom: .5em; } .rally-workprod .owner { margin-bottom: .5em; } /** tasks **/ .rally-task { padding: .8em; font-size: .9em; text-align: left; border: 1px solid #999; } .rally-task .name { font-size: 1.3em; margin-bottom: .5em; } .rally-task .owner { margin-bottom: .5em; } .rally-task .image { float: left; } .rally-task .actions { float: right; position: relative; top: -0.3em; right: -0.3em; } .rally-task .actions img { display: block; margin-bottom: .4em; cursor: pointer; } .rally-task .rally-time { width: 45px; } .rally-task-blocked { border: 1px solid #f00; } .rally-task-blocked img.blocked-icon { float: right; position: relative; bottom: -0.3em; right: -0.3em; } .rally-task-accepted { background-color: #ececec; border: 1px solid #cbcbcb; } /** filter bar **/ #filter { width: 100%; background-color: #efefdf; text-align: left; font-size: .9em; padding: .2em 0; } #filter span { padding: 0 1em; } #filter #change_iteration, #filter #filter_user { border-right: 1px solid #ccc; } #filter #proj_scope_up, #filter #proj_scope_down { border-left: 1px solid #ccc; } #project { float: right; } /** status box - displays messages while loading **/ #status { width: 50%; text-align: right; float: left; position: absolute; top: 0; left: 0; font-size: 11px; font-style: italic; font-weight: bold; } .show-progress { cursor: progress; } </style> </head> <body> <div id="wrapper"> <div id="status"></div> <div id="header"> <div id="title"><h3>Task Board</h3></div> <div id="info"></div> <div class="clear"></div> </div> <div id="filter"> <div id="project"> <span id="proj_name"></span> <span id="proj_scope_up"> Scope Up: <input type="checkbox" id="proj_scope_up_control"/> </span> <span id="proj_scope_down"> Scope Down: <input type="checkbox" id="proj_scope_down_control"/> </span> </div> <span id="change_iteration"> Change iteration: <select id="change_iteration_control" size="1"></select> </span> <span id="filter_user"> Show work owned by: <select id="filter_user_control" size="1"></select> </span> <span id="hide_accepted"> Hide accepted work: <input type="checkbox" id="hide_accepted_control"/> </span> </div> <div id="taskboard"></div> </div> </body> </html>