Manually adding columns to rallycardboard component - rally

I am creating a rallycardboard where each column represents a release, and the cards are Features to be scheduled into those releases. The default mechanics of the component render all available releases as columns on the board. For our particular application this is unreasonable, since there are thousands of Releases in our workspace.
I was able to overwrite the addColumn method to only include a column if it is a Release which at least one Feature from the group is assigned to. The next step is to make it so that a user can manually add a Release column that doesn't currently have any assigned work. To do this, I stored all the excluded columns from the first step and created a combo box with those values. I would like it so that when the user selects a Release from the combo box, that Release column is added to the board.
I was able to reconfigure my addColumn method to allow for manual override (as apposed to trying to match with an existing Feature's Release). I verified that the column was added to the boards columns by calling board.getColumns() and the configurations look the same for both the existing and added columns. However, I get an error message when calling board.renderColumns() which appears to be the result of trying to write to a container that doesn't yet exist (the column isn't created yet).
Maybe I'm going about this the wrong way. Is there another way I can more easily decide which columns to include, and which to exclude, on the rallycardboard component?

Here is an example where the board columns are based on releases that have features scheduled. To add a columns for releases that have no features currently scheduled select releases from the mulitpicker.
The app is available in this github repo.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
_releasesWithFeatures: [],
_uniqueColumns: [],
_additionalColumns: [],
_updatedColumns: [],
_cardBoard: null,
launch: function() {
var that = this;
this._releasePicker = Ext.create('Rally.ui.picker.MultiObjectPicker', {
fieldLabel: 'Choose a Release',
modelType: 'Release'
});
this.add(this._releasePicker);
this.add({
xtype: 'rallybutton',
id: 'getReleases',
text: 'Add Selected Releases',
handler: function(){
that._getSelectedReleases();
}
})
Ext.create('Rally.data.WsapiDataStore', {
model: 'PortfolioItem/Feature',
fetch: ['FormattedID','Name','Release'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'Release',
operator: '!=',
value: null
}
],
listeners: {
load: this._onScheduledFeaturesLoaded,
scope: this
}
});
},
_onScheduledFeaturesLoaded: function(store, data){
var that = this;
if (data.length !==0) {
_.each(data, function(feature){
console.log('feature ', feature.get('FormattedID'), 'scheduled for ', feature.get('Release')._refObjectName, feature.get('Release')._ref);
that._releasesWithFeatures.push(feature.get('Release'))
});
that._makeBoard();
}
else{
console.log('there are no features scheduled for a release')
}
},
_makeBoard: function(){
if (this._cardBoard) {
this._cardBoard.destroy();
}
var columns = [];
_.each(this._releasesWithFeatures, function(rel){
columns.push({value: rel._ref, columnHeaderConfig: {headerTpl: '{release}', headerData: {release: rel._refObjectName}}});
});
this._uniqueColumns = _.uniq(columns, 'value');
var cardBoard = {
xtype: 'rallycardboard',
itemId: 'piboard',
types: ['PortfolioItem/Feature'],
attribute: 'Release',
fieldToDisplay: 'Release',
columns: this._uniqueColumns
};
this._cardBoard = this.add(cardBoard);
},
_getSelectedReleases: function(){
var that = this;
var expandedColumns = [];
var selectedReleases = this._releasePicker._getRecordValue();
console.log(selectedReleases.length);
if (selectedReleases.length > 0) {
_.each(selectedReleases, function(rel) {
var releaseName = rel.get('Name');
var releaseRef = rel.get('_ref');
that._additionalColumns.push({value: releaseRef, columnHeaderConfig: {headerTpl: '{release}', headerData: {release: releaseName}}});
});
}
expandedColumns = _.union(that._uniqueColumns, that._additionalColumns);
this._updatedColumns = _.uniq(expandedColumns, 'value');
this._updateBoard();
},
_updateBoard: function(){
var that = this;
if (this._cardBoard) {
this._cardBoard.destroy();
}
var cardBoard = {
xtype: 'rallycardboard',
types: ['PortfolioItem/Feature'],
attribute: 'Release',
fieldToDisplay: 'Release',
columns: that._updatedColumns,
};
this._cardBoard = this.add(cardBoard);
}
});

Related

Using a custom Drop Down List field to set a value in a grid

I'm trying to use the Rally 2.1 SDK to set a custom data field (c_wsjf) in a grid. I have a custom drop down list that I want to check the value of (c_TimeCrticalitySizing).
I created c_TimeCrticalitySizing as a feature card field in my Rally workspace with different string values (such as "No decay"). Every drop down list value will set the custom field to a different integer. When I try to run the app in Rally I get this error:
"Uncaught TypeError: Cannot read property 'isModel' of undefined(…)"
I'm thinking the drop down list value may not be a string.
How would I check what the type of the drop down list value is?
How could I rewrite this code to correctly check the value of the drop down list so I can set my custom field to different integers?
Here's my code block for the complete app. I'm still trying to hook up a search bar so for now I directly call _onDataLoaded() from the launch() function.
// START OF APP CODE
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
featureStore: undefined,
featureGrid: undefined,
items: [ // pre-define the general layout of the app; the skeleton (ie. header, content, footer)
{
xtype: 'container', // this container lets us control the layout of the pulldowns; they'll be added below
itemId: 'widget-container',
layout: {
type: 'hbox', // 'horizontal' layout
align: 'stretch'
}
}
],
// Entry point of the app
launch: function() {
var me = this;
me._onDataLoaded();
},
_loadSearchBar: function() {
console.log('in loadsearchbar');
var me = this;
var searchComboBox = Ext.create('Rally.ui.combobox.SearchComboBox', {
itemId: 'search-combobox',
storeConfig: {
model: 'PortfolioItem/Feature'
},
listeners: {
ready: me._onDataLoaded,
select: me._onDataLoaded,
scope: me
}
});
// using 'me' here would add the combo box to the app, not the widget container
this.down('#widget-container').add(searchComboBox); // add the search field to the widget container <this>
},
// If adding more filters to the grid later, add them here
_getFilters: function(searchValue){
var searchFilter = Ext.create('Rally.data.wsapi.Filter', {
property: 'Search',
operation: '=',
value: searchValue
});
return searchFilter;
},
// Sets values once data from store is retrieved
_onDataLoaded: function() {
console.log('in ondataloaded');
var me = this;
// look up what the user input was from the search box
console.log("combobox: ", this.down('#search-combobox'));
//var typedSearch = this.down('#search-combobox').getRecord().get('_ref');
// search filter to apply
//var myFilters = this._getFilters(typedSearch);
// if the store exists, load new data
if (me.featureStore) {
//me.featureStore.setFilter(myFilters);
me.featureStore.load();
}
// if not, create it
else {
me.featureStore = Ext.create('Rally.data.wsapi.Store', {
model: 'PortfolioItem/Feature',
autoLoad: true,
listeners: {
load: me._createGrid,
scope: me
},
fetch: ['FormattedID', 'Name', 'TimeCriticality',
'RROEValue', 'UserBusinessValue', 'JobSize', 'c_TimeCriticalitySizing']
});
}
},
// create a grid with a custom store
_createGrid: function(store, data){
var me = this;
var records = _.map(data, function(record) {
//Calculations, etc.
console.log(record.get('c_TimeCriticalitySizing'));
var timecritsize = record.get('c_TimeCriticalitySizing');
//console.log(typeof timecritsize);
var mystr = "No decay";
var jobsize = record.get('JobSize');
var rroe = record.get('RROEValue');
var userval = record.get('UserBusinessValue');
var timecrit = record.get('TimeCriticality');
// Check that demoniator is not 0
if ( record.get('JobSize') > 0){
if (timecritsize === mystr){
var priorityScore = (timecrit + userval + rroe) / jobsize;
return Ext.apply({
c_wsjf: Math.round(priorityScore * 10) / 10
}, record.getData());
}
}
else{
return Ext.apply({
c_wsjf: 0
}, record.getData());
}
});
// Add the grid
me.add({
xtype: 'rallygrid',
showPagingToolbar: true,
showRowActionsColumn: true,
enableEditing: true,
store: Ext.create('Rally.data.custom.Store', {
data: records
}),
// Configure each column
columnCfgs: [
{
xtype: 'templatecolumn',
text: 'ID',
dataIndex: 'FormattedID',
width: 100,
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'WSJF Score',
dataIndex: 'c_wsjf',
width: 150
},
{
text: 'Name',
dataIndex: 'Name',
flex: 1,
width: 100
}
]
});
}
});
// END OF APP CODE
The app works great until I add the if (timecritsize === mystr) conditional.
I also use console.log() to check that I've set all values for timecritsize to "No decay"

Rally How can I export all the test cases that were run for a test set to CSV

We want to take a number test cases from previous iterations / test sets and import them into a new test set. The problem is that we cannot export the test cases from a test set into CSV or any other data format as all we get is a printable report. We have also tried copy and paste the printable report into MS Excel but it does not give a data format.
Any suggestions / HTML forms that can be used.
It is possible to copy TestSets in Rally UI and that functionality allows re-using member Test Cases from Iteration to Iteration or from Release to Release. When you copy Test Sets they preserve the member Test Cases, including Test Steps. Rally recommends this method to handle testing. If the same tests are use repeatedly, copy the test sets that contain them into the new iteration directly in the Rally UI. This resets the results for that iteration and keeps you from duplicating the testcases.
Mark's Ruby tools provide an alternative which allows selective copying of member test cases - something that does not happen when a testset is copied in the UI.
Here is a javascript example using AppSDK2. You may certainly customize it further. The main point of this example is to illustrate how to update a collection using AppSDK2.
A user may select a "source" iteration from the iteration combobox, then a testset combobox is populated with testsets scheduled for this iteration. Next a user can select a destination iteration from the third combobox that limits iterations to current and future iterations, and create a new testset scheduled for the "destination" iteration. The testcases from the "source" testset are copied to the new testset.
Currently updating collections will not work if the app is run outside of Rally, but this limitation will be corrected soon. It will allow collection updates outside of rally using CORS as long as rab run command is used. The code below should work inside Rally now. The deployment html is available from this github repo.
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
fieldLabel: 'Select a source Iteration',
labelWidth: 150,
width: 350
},
onScopeChange: function() {
if (!this.down('#parentPanel')) {
this._panel = Ext.create('Ext.panel.Panel', {
layout: 'hbox',
itemId: 'parentPanel',
componentCls: 'panel',
items: [
{
xtype: 'container',
itemId: 'pickerContainer',
},
{
xtype: 'container',
itemId: 'iterationContainer',
}
]
});
this.add(this._panel);
}
if (this.down('#testSetComboxBox')) {
this.down('#testSetComboxBox').destroy();
}
var testSetComboxBox = Ext.create('Rally.ui.combobox.ComboBox',{
itemId: 'testSetComboxBox',
storeConfig: {
model: 'TestSet',
limit: Infinity,
pageSize: 100,
autoLoad: true,
filters: [this.getContext().getTimeboxScope().getQueryFilter()]
},
fieldLabel: 'Select a TestSet',
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
this._onTestSetSelected(combobox.getRecord());
}
else{
console.log('selected iteration has no testsets');
}
},
select: function(combobox){
if (combobox.getRecord()) {
this._onTestSetSelected(combobox.getRecord());
}
},
scope: this
}
});
this.down('#pickerContainer').add(testSetComboxBox);
},
_onTestSetSelected:function(testset){
var id = testset.get('ObjectID');
this._name = testset.get('Name');
testset.self.load(id, {
fetch: ['Name','TestCases'],
callback: this._onSourceRecordRead,
scope: this
});
},
_onSourceRecordRead: function(record) {
var that = this;
that._testcases = [];
var testcaseStore = record.getCollection('TestCases',{fetch:['Name','FormattedID']});
testcaseStore.load({
callback: function() {
_.each(testcaseStore.getRange(), function(tc){
that._testcases.push(tc.data._ref);
});
console.log(that._testcases);
that._selectFutureIteration();
}
});
},
_selectFutureIteration: function(){
if (!this.down('#iterationComboxBox')) {
var iterationComboxBox = Ext.create('Rally.ui.combobox.ComboBox',{
itemId: 'iterationComboxBox',
storeConfig: {
model: 'Iteration',
limit: Infinity,
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'StartDate',
operator: '>=',
value: (new Date()).toISOString()
}
]
},
fieldLabel: 'Select a destination Iteration',
labelWidth: 150,
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
this._onFutureIterationSelected(combobox.getRecord());
}
else{
console.log('no current or future iterations');
}
},
select: function(combobox){
if (combobox.getRecord()) {
this._onFutureIterationSelected(combobox.getRecord());
}
},
scope: this
}
});
this.down('#iterationContainer').add(iterationComboxBox);
}
},
_onFutureIterationSelected:function(iteration){
var that = this;
that._iteration = iteration.data._ref;
if (!this.down('#create')) {
var createButton = Ext.create('Ext.Container', {
items: [
{
xtype : 'rallybutton',
text : 'create a testset',
itemId: 'create',
handler: function() {
that._createTestSet();
}
}
]
});
this.add(createButton);
}
},
_createTestSet: function(){
var that = this;
console.log('create testset scheduled for ', that._iteration);
Rally.data.ModelFactory.getModel({
type: 'TestSet',
success: function(model) {
that._model = model;
var ts = Ext.create(model, {
Name: that._name + 'Copy',
Iteration: that._iteration
});
ts.save({
callback: function(result, operation) {
if(operation.wasSuccessful()) {
console.log(result.get('Name'), ' ', result.get('Iteration')._refObjectName);
that._readRecord(result);
}
else{
console.log("?");
}
}
});
}
});
},
_readRecord: function(result) {
var id = result.get('ObjectID');
this._model.load(id, {
fetch: ['Name','TestCases'],
callback: this._onRecordRead(result),
scope: this
});
},
_onRecordRead: function(record, operation) {
console.log('There are ', record.get('TestCases').Count, ' in ', record.get('Name') );
var that = this;
var testcaseStore = record.getCollection('TestCases');
testcaseStore.load({
callback: function() {
testcaseStore.add(that._testcases);
testcaseStore.sync({
callback: function() {
console.log('success');
}
});
}
});
}
});
There are a couple of Ruby scripts that might help you here:
https://github.com/markwilliams970/Rally-Test-Set-Export
https://github.com/markwilliams970/Rally-Add-TestCases-TestSet
These do require a working Ruby scripting environment and some familiarity with configuring/running scripts at the command line. However, working in combination, these should be enough to accomplish your goal.

rally iteration combobox returns empty

I'm new to rally app SDK and trying to do the tutorials (from Youtube and from rally site)
when I'm trying to create an iterationComboBox the object is created but with no values ("There are no Iterations defined").
i tried to run both the video tutorial code from github (session_4_interactive_grid)
// Custom Rally App that displays Defects in a grid and filter by Iteration and/or Severity.
//
// Note: various console debugging messages intentionally kept in the code for learning purposes
Ext.define('CustomApp', {
extend: 'Rally.app.App', // The parent class manages the app 'lifecycle' and calls launch() when ready
componentCls: 'app', // CSS styles found in app.css
defectStore: undefined, // app level references to the store and grid for easy access in various methods
defectGrid: undefined,
// Entry Point to App
launch: function() {
console.log('our second app'); // see console api: https://developers.google.com/chrome-developer-tools/docs/console-api
this.pulldownContainer = Ext.create('Ext.container.Container', { // this container lets us control the layout of the pulldowns; they'll be added below
id: 'pulldown-container-id',
layout: {
type: 'hbox', // 'horizontal' layout
align: 'stretch'
}
});
this.add(this.pulldownContainer); // must add the pulldown container to the app to be part of the rendering lifecycle, even though it's empty at the moment
this._loadIterations();
},
// create iteration pulldown and load iterations
_loadIterations: function() {
this.iterComboBox = Ext.create('Rally.ui.combobox.IterationComboBox', {
fieldLabel: 'Iteration',
labelAlign: 'right',
width: 300,
listeners: {
ready: function(combobox) { // on ready: during initialization of the app, once Iterations are loaded, lets go get Defect Severities
this._loadSeverities();
},
select: function(combobox, records) { // on select: after the app has fully loaded, when the user 'select's an iteration, lets just relaod the data
this._loadData();
},
scope: this
}
});
this.pulldownContainer.add(this.iterComboBox); // add the iteration list to the pulldown container so it lays out horiz, not the app!
},
// create defect severity pulldown then load data
_loadSeverities: function() {
this.severityComboBox = Ext.create('Rally.ui.combobox.FieldValueComboBox', {
model: 'Defect',
field: 'Severity',
fieldLabel: 'Severity',
labelAlign: 'right',
listeners: {
ready: function(combobox) { // this is the last 'data' pulldown we're loading so both events go to just load the actual defect data
this._loadData();
},
select: function(combobox, records) {
this._loadData();
},
scope: this // <--- don't for get to pass the 'app' level scope into the combo box so the async event functions can call app-level func's!
}
});
this.pulldownContainer.add(this.severityComboBox); // add the severity list to the pulldown container so it lays out horiz, not the app!
},
// Get data from Rally
_loadData: function() {
var selectedIterRef = this.iterComboBox.getRecord().get('_ref'); // the _ref is unique, unlike the iteration name that can change; lets query on it instead!
var selectedSeverityValue = this.severityComboBox.getRecord().get('value'); // remember to console log the record to see the raw data and relize what you can pluck out
console.log('selected iter', selectedIterRef);
console.log('selected severity', selectedSeverityValue);
var myFilters = [ // in this format, these are AND'ed together; use Rally.data.wsapi.Filter to create programatic AND/OR constructs
{
property: 'Iteration',
operation: '=',
value: selectedIterRef
},
{
property: 'Severity',
operation: '=',
value: selectedSeverityValue
}
];
// if store exists, just load new data
if (this.defectStore) {
console.log('store exists');
this.defectStore.setFilter(myFilters);
this.defectStore.load();
// create store
} else {
console.log('creating store');
this.defectStore = Ext.create('Rally.data.wsapi.Store', { // create defectStore on the App (via this) so the code above can test for it's existence!
model: 'Defect',
autoLoad: true, // <----- Don't forget to set this to true! heh
filters: myFilters,
listeners: {
load: function(myStore, myData, success) {
console.log('got data!', myStore, myData);
if (!this.defectGrid) { // only create a grid if it does NOT already exist
this._createGrid(myStore); // if we did NOT pass scope:this below, this line would be incorrectly trying to call _createGrid() on the store which does not exist.
}
},
scope: this // This tells the wsapi data store to forward pass along the app-level context into ALL listener functions
},
fetch: ['FormattedID', 'Name', 'Severity', 'Iteration'] // Look in the WSAPI docs online to see all fields available!
});
}
},
// Create and Show a Grid of given defect
_createGrid: function(myDefectStore) {
this.defectGrid = Ext.create('Rally.ui.grid.Grid', {
store: myDefectStore,
columnCfgs: [ // Columns to display; must be the same names specified in the fetch: above in the wsapi data store
'FormattedID', 'Name', 'Severity', 'Iteration'
]
});
this.add(this.defectGrid); // add the grid Component to the app-level Container (by doing this.add, it uses the app container)
}
});
and the code from Rally site (https://help.rallydev.com/apps/2.0rc2/doc/#!/guide/first_app).
// Custom Rally App that displays Defects in a grid and filter by Iteration and/or Severity.
//
// Note: various console debugging messages intentionally kept in the code for learning purposes
Ext.define('CustomApp', {
extend: 'Rally.app.App', // The parent class manages the app 'lifecycle' and calls launch() when ready
componentCls: 'app', // CSS styles found in app.css
launch: function() {
this.iterationCombobox = this.add({
xtype: 'rallyiterationcombobox',
listeners: {
change: this._onIterationComboboxChanged,
ready: this._onIterationComboboxLoad,
scope: this
}
});
},
_onIterationComboboxLoad: function() {
var addNewConfig = {
xtype: 'rallyaddnew',
recordTypes: ['User Story', 'Defect'],
ignoredRequiredFields: ['Name', 'ScheduleState', 'Project'],
showAddWithDetails: false,
listeners: {
beforecreate: this._onBeforeCreate,
scope: this
}
};
this.addNew = this.add(addNewConfig);
var cardBoardConfig = {
xtype: 'rallycardboard',
types: ['Defect', 'User Story'],
attribute: 'ScheduleState',
storeConfig: {
filters: [this.iterationCombobox.getQueryFromSelected()]
}
};
this.cardBoard = this.add(cardBoardConfig);
},
_onBeforeCreate: function(addNewComponent, record) {
record.set('Iteration', this.iterationCombobox.getValue());
},
_onIterationComboboxChanged: function() {
var config = {
storeConfig: {
filters: [this.iterationCombobox.getQueryFromSelected()]
}
};
this.cardBoard.refresh(config);
}
});
both give me an empty iteration box.
i'm getting user stories data when running code from session 3 on the video,by creating a store of user stories. I googled it and searched here for duplicates but with no successso far, so what can be the issue?
Thanks!
I copied the code you posted, both apps, without making any changes, ran the apps and the iteration box was populated in both cases. It's not the code.
Maybe if you are getting "There are no Iterations defined" there are no iterations in your project?
The second code you posted which you copied from the example in the documentation has a bug in it and even though the iteration combobox is populated, the cards do not show on a board. DevTools console has error: "Cannot read property 'refresh' of undefined".
I have a working version of this app in this github repo.

Nested grid in ExtJS 4.1 using Row Expander

On the front-end I have a Calls grid. Each Call may have one or more Notes associated with it, so I want to add the ability to drill down into each Calls grid row and display related Notes.
On the back-end I am using Ruby on Rails, and the Calls controller returns a Calls json recordset, with nested Notes in each row. This is done using to_json(:include => blah), in case you're wondering.
So the question is: how do I add a sub-grid (or just a div) that gets displayed when a user double-clicks or expands a row in the parent grid? How do I bind nested Notes data to it?
I found some answers out there that got me part of the way where I needed to go. Thanks to those who helped me take it from there.
I'll jump straight into posting code, without much explanation. Just keep in mind that my json recordset has nested Notes records. On the client it means that each Calls record has a nested notesStore, which contains the related Notes. Also, I'm only displaying one Notes column - content - for simplicity.
Ext.define('MyApp.view.calls.Grid', {
alias: 'widget.callsgrid',
extend: 'Ext.grid.Panel',
...
initComponent: function(){
var me = this;
...
var config = {
...
listeners: {
afterrender: function (grid) {
me.getView().on('expandbody',
function (rowNode, record, expandbody) {
var targetId = 'CallsGridRow-' + record.get('id');
if (Ext.getCmp(targetId + "_grid") == null) {
var notesGrid = Ext.create('Ext.grid.Panel', {
forceFit: true,
renderTo: targetId,
id: targetId + "_grid",
store: record.notesStore,
columns: [
{ text: 'Note', dataIndex: 'content', flex: 0 }
]
});
rowNode.grid = notesGrid;
notesGrid.getEl().swallowEvent(['mouseover', 'mousedown', 'click', 'dblclick', 'onRowFocus']);
notesGrid.fireEvent("bind", notesGrid, { id: record.get('id') });
}
});
}
},
...
};
Ext.apply(me, Ext.apply(me.initialConfig, config));
me.callParent(arguments);
},
plugins: [{
ptype: 'rowexpander',
pluginId: 'abc',
rowBodyTpl: [
'<div id="CallsGridRow-{id}" ></div>'
]
}]
});

Using Rally Cardboard UI to Display Predecessor/Successor Hierarchy

I'm currently trying to write a Rally app that will display the Predecessor/Successor hierarchy for a selected User Story. To illustrate, the user will select a User Story from a Chooser UI element. Then, a three-column Cardboard UI element will be generated--the leftmost column will contain all of the selected User Story's Predecessors (in card form), the middle column will contain the selected User Story's card, and the rightmost column will contain all of the selected User Story's Successors (in card form). From there, the Predecessor and Successor cards can be removed (denoting that they won't be Predecessors or Successors for the selected User Story) and new Predecessor/Successor cards can be added (denoting that they will become new Predecessors/Successors for the selected User Story).
However, my issue is this: the Cardboard UI was designed to display sets of different values for one particular attribute, but "Predecessor" and "Successor" don't fall into this category. Is there a possible way for me to display a User Story and then get its Predecessors and Successors and populate the rest of the board? I realize that it will take a substantial amount of modifications to the original board.
I've found a way to hack it so that you can do the display. Not sure if it's worth it or not and what you want to do for adding/removing, but this might help set you on the right path.
In summary, the problem is that the cardboard/column classes are designed to work with single value fields and each column that's created does an individual query to Rally based on the column configuration. You'll need to override the rallycardboard and the rallycolumn. I'll give the full html that you can paste in below, but let's hit this one piece at a time.
If nothing else, this might be a good example of how to take the source code of rally classes and make something to override them.
Data:
The existing cardboard is given a record type and field and runs off to create a column for each valid value for the field. It gets this information by querying Rally for the stories and for the attribute definitions. But we want to use our data in a slightly different way, so we'll have to make a different data store and feed it in. So we want to use a wsapidatastore to go ahead and get the record we asked for (in this example, I have a story called US37 that has predecessors and successors). In a way, this is like doing a cross-tab in Excel. Instead of having one record (37) that's related to others, we want to make a data set of all the stories and define their relationship in a new field, which I called "_column." Like this:
Ext.create('Rally.data.WsapiDataStore', {
model: "hierarchicalrequirement",
autoLoad: true,
fetch: ['Name','Predecessors','Successors','FormattedID','ObjectID','_ref'],
filters: [ {
property: 'FormattedID', operator: 'contains', value: '37'
} ] /* get the record US37 */,
listeners: {
load: function(store,data,success) {
if ( data.length === 1 ) {
var base_story = data[0].data;
var modified_records = [];
base_story._column = "base";
modified_records.push( base_story );
Ext.Array.each( base_story.Predecessors, function( story ) {
story._column = "predecessor";
modified_records.push( story );
} );
Ext.Array.each( base_story.Successors, function(story) {
story._column = "successor";
modified_records.push( story );
} );
We push the data into an array of objects with each having a new field to define which column it should go in. But this isn't quite enough, because we need to put the data into a store. The store needs a model -- and we have to define the fields in a way that the cardrenders know how to access the data. There doesn't seem to be an easy way to just add a field definition to an existing rally model, so this should do it (the rally model has unique field information and a method called getField(), so we have to add that:
Ext.define('CardModel', {
extend: 'Ext.data.Model',
fields: [
{ name: '_ref', type: 'string' },
{ name: 'ObjectID', type: 'number'},
{ name: 'Name', type: 'string', attributeDefinition: { AttributeType: 'STRING'} },
{ name: 'FormattedID', type: 'string'},
{ name: '_column', type: 'string' },
{ name: 'ScheduleState', type: 'string' } ] ,
getField: function(name) {
if ( this.data[name] ) {
var return_field = null;
Ext.Array.each( this.store.model.getFields(), function(field) {
if ( field.name === name ) {
return_field = field;
}
} );
return return_field;
} else {
return null;
}
}
});
var cardStore = Ext.create('Ext.data.Store',{
model: 'CardModel',
data: modified_records
});
Now, we'll create a cardboard, but instead of the rally cardboard, we'll make one from a class we're going to define below ('DependencyCardboard'). In addition, we'll pass along a new definition for the columns that we'll also define below ('dependencycolumn').
var cardboard = Ext.create('DependencyCardboard', {
attribute: '_column',
store: cardStore, /* special to our new cardboard type */
height: 500,
columns: [{
xtype: 'dependencycolumn',
displayValue: 'predecessor',
value: 'predecessor',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'base',
value: 'base',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'successor',
value: 'successor',
store: cardStore
}]
});
Cardboard:
Mostly, the existing Rally cardboard can handle our needs because all the querying is done down in the columns themselves. But we still have to override it because there is one function that is causing us problems: _retrieveModels. This function normally takes the record type(s) (e.g., User Story) and based on that creates a data model that is based on the Rally definition. However, we're not using the UserStory records directly; we've had to define our own model so we could add the "_columns" field. So, we make a new definition (which we use in the create statement above for "DependencyCardboard").
(Remember, we can see the source code for all of the Rally objects in the API just by clicking on the title, so we can compare the below method to the one in the base class.)
We can keep all of the stuff that the Rally cardboard does and only override the one method by doing this:
Ext.define( 'DependencyCardboard', {
extend: 'Rally.ui.cardboard.CardBoard',
alias: 'widget.dependencycardboard',
constructor: function(config) {
this.mergeConfig(config);
this.callParent([this.config]);
},
initComponent: function() {
this.callParent(arguments);
},
_retrieveModels: function(success) {
if ( this.store ) {
this.models = [ this.store.getProxy().getModel() ];
success.apply( this, arguments );
}
}
});
Column:
Each column normally goes off to Rally and says "give me all of the stories that have a field equal to the column name". But we're passing in the store to the cardboard, so we need to override _queryForData. In addition, there is something going on about defining the column height when we do this (I don't know why!) so I had to add a little catch in the getColumnHeightFromCards() method.
_queryForData: function() {
var allRecords = [];
var records = this.store.queryBy( function( record ) {
if ( record.data._column === this.getValue() ) { allRecords.push( record ); }
}, this);
this.createAndAddCards( allRecords );
},
getColumnHeightFromCards: function() {
var contentMinHeight = 500,
bottomPadding = 30,
cards = this.query(this.cardConfig.xtype),
height = bottomPadding;
for(var i = 0, l = cards.length; i < l; ++i) {
if ( cards[i].el ) {
height += cards[i].getHeight();
} else {
height += 100;
}
}
height = Math.max(height, contentMinHeight);
height += this.down('#columnHeader').getHeight();
return height;
}
Finish
So, if you add all those pieces together, you get one long html file that we can push into a panel (and that you can keep working on to figure out how to override dragging results and to add your chooser panel for the first item. (And you can make better abstracted into its own class)).
Full thing:
<!DOCTYPE html>
<html>
<head>
<title>cardboard</title>
<script type="text/javascript" src="/apps/2.0p3/sdk.js"></script>
<script type="text/javascript">
Rally.onReady(function() {
/*global console, Ext */
Ext.define( 'DependencyColumn', {
extend: 'Rally.ui.cardboard.Column',
alias: 'widget.dependencycolumn',
constructor: function(config) {
this.mergeConfig(config);
this.callParent([this.config]);
},
initComponent: function() {
this.callParent(arguments);
},
_queryForData: function() {
var allRecords = [];
var records = this.store.queryBy( function( record ) {
if ( record.data._column === this.getValue() ) { allRecords.push( record ); }
}, this);
this.createAndAddCards( allRecords );
},
getColumnHeightFromCards: function() {
var contentMinHeight = 500,
bottomPadding = 30,
cards = this.query(this.cardConfig.xtype),
height = bottomPadding;
for(var i = 0, l = cards.length; i < l; ++i) {
if ( cards[i].el ) {
height += cards[i].getHeight();
} else {
height += 100;
}
}
height = Math.max(height, contentMinHeight);
height += this.down('#columnHeader').getHeight();
return height;
}
});
/*global console, Ext */
Ext.define( 'DependencyCardboard', {
extend: 'Rally.ui.cardboard.CardBoard',
alias: 'widget.dependencycardboard',
constructor: function(config) {
this.mergeConfig(config);
this.callParent([this.config]);
},
initComponent: function() {
this.callParent(arguments);
},
_retrieveModels: function(success) {
if ( this.store ) {
this.models = [ this.store.getProxy().getModel() ];
success.apply( this, arguments );
}
}
});
/*global console, Ext */
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
items: [ { xtype: 'container', itemId: 'outer_box' }],
launch: function() {
Ext.create('Rally.data.WsapiDataStore', {
model: "hierarchicalrequirement",
autoLoad: true,
fetch: ['Name','Predecessors','Successors','FormattedID','ObjectID','_ref'],
filters: [ {
property: 'FormattedID', operator: 'contains', value: '37'
} ],
listeners: {
load: function(store,data,success) {
if ( data.length === 1 ) {
var base_story = data[0].data;
var modified_records = [];
base_story._column = "base";
modified_records.push( base_story );
Ext.Array.each( base_story.Predecessors, function( story ) {
story._column = "predecessor";
modified_records.push( story );
} );
Ext.Array.each( base_story.Successors, function(story) {
story._column = "successor";
modified_records.push( story );
} );
Ext.define('CardModel', {
extend: 'Ext.data.Model',
fields: [
{ name: '_ref', type: 'string' },
{ name: 'ObjectID', type: 'number'},
{ name: 'Name', type: 'string', attributeDefinition: { AttributeType: 'STRING'} },
{ name: 'FormattedID', type: 'string'},
{ name: '_column', type: 'string' },
{ name: 'ScheduleState', type: 'string' } ] ,
getField: function(name) {
if ( this.data[name] ) {
var return_field = null;
Ext.Array.each( this.store.model.getFields(), function(field) {
if ( field.name === name ) {
return_field = field;
}
} );
return return_field;
} else {
return null;
}
}
});
var cardStore = Ext.create('Ext.data.Store',{
model: 'CardModel',
data: modified_records
});
var cardboard = Ext.create('DependencyCardboard', {
attribute: '_column',
store: cardStore,
height: 500,
columns: [{
xtype: 'dependencycolumn',
displayValue: 'predecessor',
value: 'predecessor',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'base',
value: 'base',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'successor',
value: 'successor',
store: cardStore
}]
});
this.down('#outer_box').add( cardboard );
}
},
scope: this
}
});
}
});
Rally.launchApp('CustomApp', {
name: 'cardboard'
});
});
</script>
<style type="text/css">
.app {
/* Add app styles here */
}
</style>
</head>
<body></body>
</html>