SDK2: sorting custom column in a rally grid - rally

I have a rally grid that shows defects. I want do add a column that shows the number of days a defect has been open.
I know can do that by adding a custom renderer in the column configs, but I would also like to sort on this column. Unfortunately, the renderer does not change the sorting of the column.
I think I might be able to use the convert() function on the store instead to create a new virtual column (in this case openAgeDays), but I'm not sure how to do this from the constructor--I presume I make some changes to storeConfig?
Does anyone have an example of how to use a convert function (assuming that this is the right way to do it) to add a new virtual, sortable column to a rally grid?
this.grid = this.add({
xtype: 'rallygrid',
model: model,
disableColumnMenus: false,
storeConfig: [...]

As is the answer in the duplicate, you can add a doSort to the column:
{dataIndex: 'Parent', name: 'Parent',
doSort: function(state) {
var ds = this.up('grid').getStore();
var field = this.getSortParam();
console.log('field',field);
ds.sort({
property: field,
direction: state,
sorterFn: function(v1, v2){
console.log('v1',v1);
console.log('v2',v2);
if (v1.raw.Parent) {
v1 = v1.raw.Parent.Name;
} else {
v1 = v1.data.Name;
}
if (v2.raw.Parent) {
v2 = v2.raw.Parent.Name;
} else {
v2 = v2.data.Name;
}
return v1.localeCompare(v2);
}
});
},
renderer: function(value, meta, record) {
var ret = record.raw.Parent;
if (ret) {
return ret.Name;
} else {
meta.tdCls = 'invisible';
return record.data.Name;
}
}
},

Related

Custom rendering of Agile Central grid field

I'm trying to create a custom grid with columns for Feature Predecessors and Successors.
I've managed to extract the data and display the relevant formatted IDs of the pre+suc in clear text. Now I would like to format these as "standard" FormattedIDs with QDP/click options.
My display looks like this, is this the right path, what should I return in order to have correct formatting?
var myGrid = Ext.create('Ext.Container', {
items: [
{
xtype: 'rallygrid',
columnCfgs: [
'FormattedID',
'Name',
{ // Column 'Successors'
xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate'),
dataIndex: 'Successors',
renderer: function(value, metaData, record) {
//console.log('Display in renderer: ', record.successorStore);
var mFieldOutputSuc = '';
var i;
var mDependency;
for (i = 0; i < record.successorStore.getCount(); i++) {
mDependency = record.successorStore.getAt(i);
console.log('mDependency = ', mDependency);
mFieldOutputSuc = mFieldOutputSuc + mDependency.get('FormattedID') + '<br>'; // Correct return value?
}
return mFieldOutputSuc;
}, //renderer: function(value, metaData, record) {
}, // Column 'Successors'
I'd check out this utility method: https://help.rallydev.com/apps/2.1/doc/#!/api/Rally.nav.DetailLink-method-getLink
You should be able to just put that in there where you have your //Correct return value? comment:
mFieldOutputSuc += Rally.nav.DetailLink.getLink({
record: mDependency
});
There are some more config options you can pass as well, to customize the link further, but I think that should get you started...

How to create new TimeEntryValue in Rally

I'm fairly new to the Rally API and JS, and Stackoverflow for that matter. I have been using Stackoverflow to answer all of my questions so far, but I can't seem to find anything about adding new TimeEntryValues.
I am building an app that allows to add new TimeEntryValues. I can add or load TimeEntryItems but for TimeEntryValues, I ever only seem to post the Hours field when looking at the trace in the browser.
Here is a simplified code that exhibits the same problem.
launch: function(){
//For this example, pre-define Time Entry Reference, Date, and Hour value
var myTimeEntryItem = "/timeentryitem/1234";
var myDateValue = "2016-05-20T00:00:00.000Z";
var myHours = 2.5;
//Check if Time Entry Value (TEV) already exists
var TEVstore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TimeEntryValue',
fetch: ['ObjectID','TimeEntryItem','Hours','DateVal'],
filters: [{
property: 'TimeEntryItem',
operator: '=',
value: myTimeEntryItem
},
{
property: 'DateVal',
operator: '=',
value: myDateValue
}],
autoLoad: true,
listeners: {
load: function(TEVstore, tevrecords, success) {
//No record found - TEV does not exist
if (tevrecords.length === 0) {
console.log("Creating new TEV record");
Rally.data.ModelFactory.getModel({
type: 'TimeEntryValue',
success: function(tevModel) {
var newTEV = Ext.create(tevModel, {
DateVal: myDateValue,
Hours: myHours,
TimeEntryItem: myTimeEntryItem
});
newTEV.save({
callback: function(result, operation) {
if(operation.wasSuccessful()) {
console.log("Succesful Save");
//Do something here
}
}
});
}
});
} else {
console.log("TEV Record exists.");
//Do something useful here
}
}
},
scope: this
});
}
Any hints what I am doing wrong are greatly appreciated.
Thanks
This is actually a longstanding defect in App SDK caused by a mismatch in the WSAPI attribute metadata and the client side models used for persisting data to the server.
Basically what's happening is the DateVal and TimeEntryItem fields are marked required and readonly, which doesn't make sense. Really, they need to be writable on create and then readonly after that.
So all you need to do in your app is before you try to save your new TimeEntryValue just mark the DateVal and TimeEntryItem fields as persistable and you should be good to go.
//workaround
tevModel.getField('DateVal').persist = true;
tevModel.getField('TimeEntryItem').persist = true;
//proceed as usual
var newTEV = Ext.create(tevModel, {
DateVal: myDateValue,
Hours: myHours,
TimeEntryItem: myTimeEntryItem
});
// ...

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>

In an ExtJS Grid, how do I get access to the data store fields that are part of the sort set

How do I get access to the columns/datastore fields that are part of the sort set.
I am looking to modify the a grid's sort parameters for remote sorting. I need the remote sort param's sort key to match the column's field's mapping property. I need these things to happen though the normal 'column header click sorts the data' functionality.
Remote sorting and field mapping (ExtJS 4.1)
This functionality seems not to be implemented in ExtJS. Here is a solution using the encodeSorters function provided since ExtJS 4. Accessing fields map throught the model's prototype is a bit dirty but it does the job :
var store = Ext.create('Ext.data.Store', {
...,
proxy: {
...,
encodeSorters: function (sorters) {
var model = store.proxy.model,
map = model.prototype.fields.map;
return Ext.encode(Ext.Array.map(sorters, function (sorter) {
return {
property : map[sorter.property].mapping || sorter.property,
direction: sorter.direction
};
}));
}
}
});
However, it would be more relevant to override the original method :
Ext.data.proxy.Server.override({
encodeSorters: function(sorters) {
var min, map = this.model.prototype.fields.map;
min = Ext.Array.map(sorters, function (sorter) {
return {
property : map[sorter.property].mapping || sorter.property,
direction: sorter.direction
};
});
return this.applyEncoding(min);
}
});
Assuming you are using simpleSortMode, you could do something like this in your store.
listeners: {
beforeload: function( store, operation, eOpts ) {
if (store.sorters.length > 0) {
var sorter = store.sorters.getAt(0),
dir = sorter.direction,
prop = sorter.property,
fields = store.model.getFields(),
i,
applyProp = prop;
for (i = 0; i < fields.length; i++) {
if (fields[i].name == prop) {
applyProp = fields[i].mapping || prop;
break;
}
}
//clearing the sorters since the simpleSortMode is true so there will be only one sorter
store.sorters.clear();
store.sorters.insert(0, applyProp, new Ext.util.Sorter({
property : applyProp,
direction: dir
}));
}
}
},

How do I enable only new rows in a Ext 4 Grid Panel?

So I have a ExtJs 4 Grid Panel that has two columns.
Column ONE is a ComboBox with a custom renderer
Ext.ux.comboBoxRenderer = function(combo) {
return function(value) {
var idx = combo.store.find(combo.valueField, value);
var rec = combo.store.getAt(idx);
return (rec == null ? '<img src="/js/extjs4/resources/themes/images/default/grid/loading.gif" />' : rec.get(combo.displayField) );
};
}
Column TWO is a numeric column
{
allowBlank: false,
header: 'Time',
dataIndex: 'tedPaidTime',
align: 'right',
field: {
xtype: 'numberfield',
allowBlank: false,
minValue: 0,
maxValue: 100000
},
flex: 0.20,
selectOnFocus:true
}
Now, when I load the DataStore, I want the FIRST column to be read only. Which is easy if I set the ReadOnly flag.
However, I have a button that adds a NEW record to store and I want that new record to NOT have the readonly flag.
So basically, once you change column one and save, you can not alter it. Only able to alter new records before they are saved.
The business rules here require them to delete the old record if it's incorrect.
Thanks.
I was able to solve this myself.
Basically, I recorded the combo on the selectionchange event.
Then, in the focus event, I checked to see if it had a required data key from the store. If not, then I assume a new row and ran the following:
focus: function(combo) {
var cb = combo;
if(newTEDetailRecord) {
if(newTEDetailRecord.data["activityKey"] == "") {
console.log("newTEDetailRecord", newTEDetailRecord);
cb.setEditable(true);
cb.setReadOnly(false);
cb.focus();
cb.selectText();
} else {
cb.setReadOnly(true);
}
}
},