How to show task revisions in custom grid? - rally

I have a custom grid that displays open tasks filtered by (Owner = some-user#company.com).
I would like to include the last revision for each task in a custom grid, but the Revision column is not available on the settings dialog. How to traverse from Revision History to individual revisions?

It can't be done with a custom grid, but can be done with a custom code. Here is an app example that populates a grid based on a selection in the UserSearchComboBox , and then displays the last revision of a selected task on a click event.
You may copy the html file into a custom page from this github repo:
Here is the js file:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var context = this.getContext ();
var currentProject = context.getProject()._ref;
var panel = Ext.create('Ext.panel.Panel', {
layout: 'hbox',
itemId: 'parentPanel',
componentCls: 'panel',
items: [
{
xtype: 'rallyusersearchcombobox',
fieldLabel: 'SELECT USER:',
project: currentProject,
listeners:{
ready: function(combobox){
this._onUserSelected(combobox.getRecord());
},
select: function(combobox){
if (this.down('#c').html !== 'No task is selected') {
Ext.getCmp('c').update('No task is selected');
}
this._onUserSelected(combobox.getRecord());
},
scope: this
}
},
{
xtype: 'panel',
title: 'Tasks',
width: 600,
itemId: 'childPanel1'
},
{
xtype: 'panel',
title: 'Last Revision',
width: 600,
itemId: 'childPanel2'
}
],
});
this.add(panel);
this.down('#childPanel2').add({
id: 'c',
padding: 10,
maxWidth: 600,
maxHeight: 400,
overflowX: 'auto',
overflowY: 'auto',
html: 'No task is selected'
});
},
_onUserSelected:function(record){
var user = record.data['_ref'];
if(user){
var filter = Ext.create('Rally.data.QueryFilter', {
property: 'Owner',
operator: '=',
value: user
});
filter = filter.and({
property: 'State',
operator: '<',
value: 'Completed'
});
filter.toString();
Ext.create('Rally.data.WsapiDataStore', {
model: 'Task',
fetch: [ 'DragAndDropRank','FormattedID','Name','State','RevisionHistory'],
autoLoad: true,
filters : [filter],
sorters:[
{
property: 'DragAndDropRank',
direction: 'ASC'
}
],
listeners: {
load: this._onTaskDataLoaded,
scope: this
}
});
}
},
_onTaskDataLoaded: function(store, data) {
var customRecords = [];
Ext.Array.each(data, function(task, index) {
customRecords.push({
_ref: task.get('_ref'),
FormattedID: task.get('FormattedID'),
Name: task.get('Name'),
RevisionID: Rally.util.Ref.getOidFromRef(task.get('RevisionHistory')),
});
}, this);
this._updateGrid(store,data);
},
_updateGrid: function(store, data){
if (!this.down('#g')) {
this._createGrid(store,data);
}
else{
this.down('#g').reconfigure(store);
}
},
_createGrid: function(store,data){
var that = this;
var g = Ext.create('Rally.ui.grid.Grid', {
id: 'g',
store: store,
enableRanking: true,
columnCfgs: [
{text: 'Formatted ID', dataIndex: 'FormattedID'},
{text: 'Name', dataIndex: 'Name'},
{text: 'State', dataIndex: 'State'},
{text: 'Last Revision',
renderer: function (v, m, r) {
var id = Ext.id();
Ext.defer(function () {
Ext.widget('button', {
renderTo: id,
text: 'see',
width: 50,
handler: function () {
console.log('r', r.data);
that._getRevisionHistory(data, r.data);
}
});
}, 50);
return Ext.String.format('<div id="{0}"></div>', id);
}
}
],
height: 400,
});
this.down('#childPanel1').add(g);
},
_getRevisionHistory: function(taskList, task) {
this._task = task;
this._revisionModel = Rally.data.ModelFactory.getModel({
type: 'RevisionHistory',
scope: this,
success: this._onModelCreated
});
},
_onModelCreated: function(model) {
model.load(Rally.util.Ref.getOidFromRef(this._task.RevisionHistory._ref),{
scope: this,
success: this._onModelLoaded
});
},
_onModelLoaded: function(record, operation) {
record.getCollection('Revisions').load({
fetch: true,
scope: this,
callback: function(revisions, operation, success) {
this._onRevisionsLoaded(revisions, record);
}
});
},
_onRevisionsLoaded: function(revisions, record) {
var lastRev = _.first(revisions).data;
console.log('_onRevisionsLoaded: ',lastRev.Description, lastRev.RevisionNumber, lastRev.CreationDate );
this._displayLastRevision(lastRev.Description,lastRev.RevisionNumber, lastRev.CreationDate );
},
_displayLastRevision:function(desc, num, date){
Ext.getCmp('c').update('<b>' + this._task.FormattedID + '</b><br/><b>Revision CreationDate: </b>' + date +'<br /><b>Description:</b>' + desc + '<br /><b>Revision Number:</b>' + num + '<br />');
}
});

Related

How to get status of portfolio saga item by story hours?

Does anyone know an easy way to get a percent done by story hours related to 'Portfolioitem/Saga'? The existing portfolio items app in CA only shows percent done by story plan estimate and I'd like to get the task hours details as well.
I've written the following custom app to include the data but my table gets loaded before the data is returned. Please help!
Ext.define('Rally.app.PortfolioGrid', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
console.log('Working');
this._loadComboBox();
},
_loadComboBox : function() {
this.searchComboBox = Ext.create('Rally.ui.combobox.ArtifactSearchComboBox', {
fieldLabel: 'Saga',
noEntryText: '',
emptyText: 'Enter saga ID or keyword...',
//grow: true,
//allowNoEntry: false,
storeConfig: {
autoLoad: true,
models: ['PortfolioItem/Saga']
},
listeners: {
select: function(combobox, records) {
this._loadData();
},
scope: this
}
});
this.add(this.searchComboBox);
},
_loadData: function () {
var selectedID = this.searchComboBox.getRecord().get('FormattedID');
console.log('Selected Saga', selectedID);
var myFilters = [
{
property: 'FormattedID',
operation: '=',
value: selectedID
},
];
if (this.myStore) {
console.log('store exists');
this.myStore.setFilter(myFilters);
this.myStore.load();
// create store
} else {
this.myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'PortfolioItem/Saga',
autoLoad: true,
fetch: ['FormattedID', 'Name', 'Children', 'Release', 'State', 'PercentDoneByStoryPlanEstimate', 'PercentDoneByStoryCount', 'Project', 'Owner'],
filters: myFilters,
listeners: {
load: this._onDataLoaded,
scope: this
}
});
}
},
_onDataLoaded: function(store, data){
var features = [];
var pendingstories = data.length;
Ext.Array.each(data, function(feature) {
var f = {
FormattedID: feature.get('FormattedID'),
Name: feature.get('Name'),
_ref: feature.get("_ref"),
State: (feature.get('State') && feature.get('State').Name) || ' ',
Project: (feature.get('Project') && feature.get('Project').Name) || ' ',
Owner: (feature.get('Owner') && feature.get('Owner')._refObjectName) || 'No Owner',
PercentDoneByStoryPlanEstimate: Math.floor(feature.get('PercentDoneByStoryPlanEstimate') * 100) + '%',
PercentDoneByStoryCount: Math.floor(feature.get('PercentDoneByStoryCount') * 100) + '%',
Children: [],
totalPlannedHours: 0,
totalHoursRemaining: 0,
totalHoursCompleted: 0,
Percentage: 0
};
var sagaFeatures = feature.getCollection('Children');
sagaFeatures.load({
fetch: ['FormattedID', 'Parent', 'UserStories', 'DirectChildrenCount'],
callback: function(records, operation, success){
Ext.Array.each(records, function(child){
var s = {
FormattedID: child.get('FormattedID'),
};
if (child.get('DirectChildrenCount') > 0) {
child.getCollection('UserStories').load({
fetch: ['FormattedID', 'TaskEstimateTotal', 'TaskRemainingTotal'],
callback: function(records, operation, success) {
Ext.Array.each(records, function(us) {
f.totalPlannedHours += us.get('TaskEstimateTotal');
f.totalHoursRemaining += us.get('TaskRemainingTotal');
f.totalHoursCompleted = f.totalPlannedHours - f.totalHoursRemaining
console.log("Total Hours Completed", f.totalHoursRemaining)
f.Percentage = Math.floor((f.totalHoursCompleted / f.totalPlannedHours) * 100) + '%';
}, this);
}, scope: this});
}
f.Children.push(s);
}, this);
},
scope: this
});
features.push(f);
--pendingstories;
if (pendingstories === 0) {
this._createGrid(features);
}
}, this);
this._createGrid(features);
},
_createGrid: function(features) {
var myCustomStore = Ext.create('Rally.data.custom.Store', {
data: features
//pageSize: 100,
});
if (!this.grid) {
this.grid = this.add({
xtype: 'rallygrid',
flex: 1,
store: myCustomStore,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'State', dataIndex: 'State'
},
{
text: 'Saga Features', dataIndex: 'Children',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(child){
html.push('' + child.FormattedID + '')
});
return html.join(', ');
}
},
{
text: 'Percent Done By Story Plan Estimate', dataIndex: 'PercentDoneByStoryPlanEstimate'
},
{
text: 'Percent Done By Story Count', dataIndex: 'PercentDoneByStoryCount'
},
{
text: 'Total Planned Hours', dataIndex: 'totalPlannedHours'
},
{
text: 'Total Completed Hours', dataIndex: 'totalHoursCompleted'
},
{
text: 'Percent Done by Story Hours', dataIndex: 'Percentage'
},
{
text: 'Project', dataIndex: 'Project'
},
{
text: 'Owner', dataIndex: 'Owner'
}
]
});
}
else {
this.grid.reconfigure(myCustomStore);
}
}
});
Can you load the data from the bottom up instead and save yourself all the many nested loops and store loads?
I think you should be able to just create a store of stories beneath the selected saga...
this.myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'UserStory',
autoLoad: true,
limit: Infinity,
pageSize: 2000,
fetch: ['FormattedID', 'Name', 'TaskEstimateTotal', 'TaskRemainingTotal', 'Feature', 'Parent', 'Children'],
filters: [{ property: 'Feature.Parent', operator: '=', value: this.searchComboBox.getRecord().get('_ref')}],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
And then just crunch your data into the appropriate data structure, bucketing them by Feature. Something like this?
_onDataLoaded: function(store) {
var sagas = {};
var stories = store.getRange();
_.each(stories, function(story) {
var feature = store.get('Feature'),
saga = feature.Parent;
if (!sagas[saga._ref]) {
sagas[saga._ref] = Ext.clone(saga);
}
var featureChildren = sagas[saga._ref].Children.values = sagas[saga._ref].Children.values || {};
if (!featureChildren[feature._ref]) {
featureChildren[feature._ref] = Ext.clone(feature);
}
var storyChildren = featureChildren[feature._ref].UserStories.values = featureChildren[feature._ref].UserStories.values || {};
if (!storyChildren[story._ref]) {
storyChildren[story._ref] = Ext.clone(story);
}
});
//now you have an object of sagas, just needs to be an array for your custom store data
var customStoreData = _.values(sagas);
this._createGrid(customStoreData);
}
Something like that should get you close. You'll need to move around your code a little bit where you're calculating your rollup totals, and your renderer for the Children column will be a tiny bit different- just loop over _.values(value.values) instead.

Report to display all top level stories or portlio items in blocked status and include blocked reason

I would love a report that would display all top level user stories (or better yet portfolio items, if possible) that have one or more tasks in blocked status, with the blocked reasons included in the report. This would be across an entire release
I am thinking that many others would also want to view data like this as well, however I couldn't find this in any of the canned reports. Anyone familiar with a way to do this?
Thanks
Here is a custom app that builds a grid of blocked tasks with their workproducts. When a workproduct has a epic (parent) story and a parent feature, those are also shown in respective columns.
The full code is available in this github repo. You may copy/paste the html file into a custom page in Rally.
The js source:
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
labelWidth: 100,
width: 300
},
onScopeChange: function() {
var filter = this.getContext().getTimeboxScope().getQueryFilter();
filter = filter.and({
property: 'Blocked',
value: true
});
filter.toString();
Ext.create('Rally.data.WsapiDataStore', {
model: 'Task',
fetch: ['ObjectID', 'FormattedID','Name', 'WorkProduct','Blocked', 'BlockedReason', 'Parent', 'Feature'],
autoLoad: true,
filters: [filter],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
},
_onDataLoaded: function(store, data){
var tasks = [];
Ext.Array.each(data, function(task) {
console.log('Blocked',task.get('Blocked'));
console.log(task.get('WorkProduct')._type);
var t = {
ObjectID: task.get('ObjectID'),
FormattedID: task.get('FormattedID'),
Name: task.get('Name'),
_ref: task.get('_ref'),
WorkProduct: task.get('WorkProduct'),
Blocked: task.get('Blocked'),
BlockedReason: task.get('BlockedReason'),
WorkProductType: task.get('WorkProduct')._type
};
tasks.push(t);
}, this);
this._createGrid(tasks);
},
_createGrid: function(tasks) {
var that = this;
var myStore = Ext.create('Rally.data.custom.Store', {
data: tasks,
pageSize: 100,
});
if (!this.grid) {
this.grid = this.add({
xtype: 'rallygrid',
store: myStore,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: '', dataIndex: 'Name'
},
{
text: 'WorkProduct', dataIndex: 'WorkProduct',
renderer: function(val, meta, record) {
var type;
if(record.get('WorkProduct')._type === "HierarchicalRequirement") {
type = 'userstory';
}
else if(record.get('WorkProduct')._type === "Defect"){
type = 'defect';
}
return '' + record.get('WorkProduct').FormattedID + '';
}
},
{
text: 'Blocked Reason', dataIndex: 'BlockedReason'
},
{
text: 'Parent of WorkProduct', dataIndex: 'WorkProduct',
renderer: function(val, meta, record) {
if(record.get('WorkProduct')._type !== "HierarchicalRequirement") {
return 'n/a'
}
else{
return '' + record.get('WorkProduct').Parent.FormattedID + '';
}
}
},
{
text: 'Feature of WorkProduct', dataIndex: 'WorkProduct',
renderer: function(val, meta, record) {
if(record.get('WorkProduct')._type !== "HierarchicalRequirement") {
return 'n/a'
}
else{
return '' + record.get('WorkProduct').Feature.FormattedID + '';
}
}
}
]
});
}else{
this.grid.reconfigure(myStore);
}
}
});

Detailed view for each test set in a Rally project

I am trying to figure out a way to have a Rally summary page displaying all test sets per the globally chosen project and particulary the exact numbers of pass/totals per test set.
I can display this using TestCaseStatus but the strings returned are not what I want. I read through some posts and it seems that the only way to get this kind of details is to iterate through all test set test cases and check if they are passing or not on the client side; also to count them up.
Can anyone provide a working example of how to iterate through the test set test cases and count their last verdict BUT only for the current project not the last verdict in general?
An example of a custom add that displays a grid of Test Sets based on a Project selection and then displays a grid of associated test cases and their test case results when a user double clicks on a row in a first grid is available in this github repo. You may copy the html file to a custom html page you create for this purpose.
Here is a js file:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var panel = Ext.create('Ext.panel.Panel', {
layout: 'hbox',
itemId: 'parentPanel',
componentCls: 'panel',
items: [
{
xtype: 'rallyprojectpicker',
fieldLabel: 'select project',
listeners:{
change: function(combobox){
if ( this.down('#g')) {
console.log('grid exists');
Ext.getCmp('g').destroy();
console.log('grid deleted');
}
this.onProjectSelected(combobox.getSelectedRecord());
},
scope: this
}
},
{
xtype: 'panel',
title: 'Test Sets',
itemId: 'childPanel1'
},
{
xtype: 'panel',
title: 'Test Cases',
width: 600,
itemId: 'childPanel2'
}
],
});
this.add(panel);
},
onProjectSelected:function(record){
var project = record.data['_ref'];
console.log('project', project);
var testSetStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TestSet',
fetch: ['FormattedID','Name', 'Project', 'TestCaseStatus', 'TestCases'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'Project',
value: project
}
],
listeners: {
load: this.onTestSetsLoaded,
scope: this
}
});
},
onTestSetsLoaded:function(store, data){
var testSets = [];
Ext.Array.each(data, function(testset) {
var ts = {
FormattedID: testset.get('FormattedID'),
_ref: testset.get('_ref'),
Name: testset.get('Name'),
TestCaseCount: testset.get('TestCases').Count,
TestCaseStatus: testset.get('TestCaseStatus')
};
testSets.push(ts);
});
this.updateGrid(testSets);
},
updateGrid: function(testSets){
if (this.down('#g2')) {
console.log('g2 exists');
var store = this.down('#g2').getStore();
store.removeAll();
}
var store = Ext.create('Rally.data.custom.Store', {
data: testSets,
pageSize: 100
});
if (!this.down('#g')) {
this.createGrid(store);
}
else{
this.down('#g').reconfigure(store);
}
},
createGrid: function(store){
console.log("load grid", store);
var that = this;
var g = Ext.create('Rally.ui.grid.Grid', {
id: 'g',
store: store
});
var g = Ext.create('Rally.ui.grid.Grid', {
id: 'g',
store: store,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'Test Case Count', dataIndex: 'TestCaseCount',
},
{
text: 'TestCaseStatus', dataIndex: 'TestCaseStatus'
}
],
listeners: {
celldblclick: function( grid, td, cellIndex, record, tr, rowIndex){
var id = grid.getStore().getAt(rowIndex).get('FormattedID');
console.log('id', id);
that.getTestCases(id);
}
}
});
this.down('#childPanel1').add(g);
},
getTestCases:function(id){
var selectedTestSetStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TestSet',
fetch: ['FormattedID','Name', 'TestCases'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'FormattedID',
operator: '=',
value: id
}
],
listeners: {
load: this.onSelectedTestSetLoaded,
scope: this
}
});
},
onSelectedTestSetLoaded:function(store, data){
console.log('store',store);
console.log('data',data);
var selectedTestSets = [];
var pendingTestCases = data.length;
if (data.length ===0) {
this.createTestSetGrid(selectedTestSets);
}
Ext.Array.each(data, function(selectedTestset){
var ts = {
FormattedID: selectedTestset.get('FormattedID'),
TestCaseCount: selectedTestset.get('TestCases').Count,
TestCases: [],
ResultCount: 0
};
var testCases = selectedTestset.getCollection('TestCases', {fetch: ['FormattedID','ObjectID', 'Results']});
console.log('testCases:', selectedTestset.get('TestCases').Count, testCases);
testCases.load({
callback: function(records, operation, success){
Ext.Array.each(records, function(testcase){
console.log("testcase.get('FormattedID')", testcase.get('FormattedID'));
console.log("testcase.get('Results').Count", testcase.get('Results').Count);
ts.ResultCount = testcase.get('Results').Count;
console.log('ts.ResultCount', ts.ResultCount);
ts.TestCases.push({_ref: testcase.get('_ref'),
FormattedID: testcase.get('FormattedID'),
ObjectID: testcase.get('ObjectID')
});
}, this);
--pendingTestCases;
if (pendingTestCases === 0) {
this.makeTestCaseStore(ts.TestCases);
}
},
scope: this
});
console.log('ts', ts);
selectedTestSets.push(ts);
},this);
},
makeTestCaseStore:function(testcases){
console.log('makeTestCaseStore'); //ok
if (testcases.length>0) {
var idArray = [];
_.each(testcases, function(testcase){
console.log(testcase);
console.log('OID', testcase['ObjectID']);
idArray.push(testcase['ObjectID']);
});
console.log('idArray',idArray);
var filterArray = [];
_.each(idArray, function(id){
filterArray.push(
{
property: 'ObjectID',
value:id
}
)
});
console.log('filterArray', filterArray); //ok
var filters = Ext.create('Rally.data.QueryFilter', filterArray[0]);
filterArray = _.rest(filterArray,1);
_.each(filterArray, function(filter){
filters = filters.or(filter)
},1);
var testCaseStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TestCase',
fetch: ['FormattedID','Name', 'ObjectID', 'Results'],
pageSize: 100,
autoLoad: true,
filters: [filters],
listeners: {
load: this.onTestCasesLoaded,
scope: this
}
});
}
else{
if (this.down('#g2')) {
var store = this.down('#g2').getStore();
store.removeAll();
}
}
},
onTestCasesLoaded:function(store,data){
console.log('onTestCasesLoaded');
console.log('store',store);
console.log('data',data);
var testCases = [];
var pendingResults = data.length;
Ext.Array.each(data, function(testcase) {
var tc = {
FormattedID: testcase.get('FormattedID'),
_ref: testcase.get('_ref'),
Name: testcase.get('Name'),
ResultsCount: testcase.get('Results').Count,
Results: []
};
var results = testcase.getCollection('Results');
results.load({
fetch: ['Verdict','Date','Build'],
callback: function(records, operation, success){
Ext.Array.each(records, function(result){
tc.Results.push({_ref: result.get('_ref'),
Verdict: result.get('Verdict'),
Date: result.get('Date'),
Build: result.get('Build'),
});
},this);
--pendingResults;
if (pendingResults === 0) {
this.updateGrid2(testCases);
}
},
scope:this
});
testCases.push(tc);
}, this);
},
updateGrid2: function(testCases){
console.log(testCases);
var store = Ext.create('Rally.data.custom.Store', {
data: testCases,
pageSize: 100
});
if (!this.down('#g2')) {
this.createGrid2(store);
}
else{
this.down('#g2').reconfigure(store);
}
},
createGrid2: function(store){
console.log("load grid", store);
var that = this;
var g2 = Ext.create('Rally.ui.grid.Grid', {
id: 'g2',
store: store,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name',
},
{
text: 'Results Count', dataIndex: 'ResultsCount',
},
{
text: 'Results', dataIndex: 'Results', flex:1,
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(result){
html.push('<b>Verdict:</b> ' + result.Verdict + '<br />' + '<b>Date:</b> ' + Rally.util.DateTime.toIsoString(result.Date,true) + '<br />' + '<b>Build</b> ' + result.Build + '<br />')
});
return html.join('<br /><br />');
}
}
]
});
this.down('#childPanel2').add(g2);
}
});

How do I Query For Defects where with a requirement under a particular feature

When using a custom grid query based on defects I often filter based on the tags of the linked user story. I would like to use the actual feature hierarchy. e.g. Show all defects where the linked story is under a given feature or initiative. I could not work this out from looking at the documentation
Tags attribute exists on Artifact object from which Requirement inherits, hence Requirement.Tags can be traversed. Feature attribute does not exist on Requirement. It exists on HierarchicalRequirement, which inherits from Requirement, hence Requirement.Feature cannot be traversed.
In this context a custom grid may not be a suitable choice. But a custom app can be written that shows all those relationships. Here is a custom app that has two comboboxes: a Release and a Feature. The Feature combobox is populated based on the selection in the Release combobox. When a feature is selected from the second combobox a grid is populated with the feature's child stories and defects (if any) associated with those stories.
You may see the full code in this repo, and copy the html file into a custom page.
Here is the js file:
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'release',
comboboxConfig: {
fieldLabel: 'Select a Release:',
labelWidth: 100,
width: 300
},
addContent: function() {
this._makeCombobox();
},
onScopeChange: function() {
this._makeCombobox();
},
_makeCombobox: function() {
if (this.down('#features')) {
this.down('#features').destroy();
}
var features = Ext.create('Rally.ui.combobox.ComboBox',{
id: 'features',
storeConfig: {
model: 'PortfolioItem/Feature',
fetch: ['FormattedID','Name','Release', 'UserStories'],
pageSize: 100,
autoLoad: true,
filters: [this.getContext().getTimeboxScope().getQueryFilter()]
},
fieldLabel: 'select Feature',
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
console.log('ready',combobox.getRecord().get('_ref'));
this._onFeatureSelected(combobox.getRecord());
}
else{
console.log('selected release has no features');
if (this.down('#grid')) {
this.down('#grid').destroy();
}
}
},
select: function(combobox){
if (combobox.getRecord()) {
console.log('select',combobox.getRecord().get('_ref'));
this._onFeatureSelected(combobox.getRecord());
}
},
scope: this
}
});
this.add(features);
},
_onFeatureSelected:function(feature){
console.log('feature', feature.get('Name'));
var f = {
FormattedID: feature.get('FormattedID'),
Name: feature.get('Name'),
_ref: feature.get("_ref"),
UserStories: []
};
var collection = feature.getCollection('UserStories', {fetch: ['Name','FormattedID','Owner', 'Defects']});
var that = this;
var count = collection.getCount();
console.log(count);
var stories = [];
var pendingStories = count;
collection.load({
callback: function(records, operation, success){
Ext.Array.each(records, function(story){
var s = {
FormattedID: story.get('FormattedID'),
Name: story.get('Name'),
_ref: story.get("_ref"),
DefectCount: story.get('Defects').Count,
Defects: []
};
var defects = story.getCollection('Defects');
var defectcount = defects.getCount();
var pendingDefects = defectcount;
defects.load({
fetch: ['FormattedID'],
callback: function(records, operation, success){
Ext.Array.each(records, function(defect){
s.Defects.push({_ref: defect.get('_ref'),
FormattedID: defect.get('FormattedID')
});
}, this);
--pendingDefects;
if (pendingDefects === 0) {
console.log(story.get('FormattedID') + ' - ' + story.get('Name'));
--pendingStories;
if (pendingStories === 0) {
console.log('stories inside callback',stories);
}
}
console.log('makeGrid');
that._makeGrid(stories);
},
scope: this
});
stories.push(s);
}, this);
}
});
},
_makeGrid: function(stories) {
var c = Ext.create('Ext.Container', {
layout: {
type: 'absolute'
},
x: 400
});
this.add(c);
this._store = Ext.create('Rally.data.custom.Store', {
data: stories,
pageSize: 100,
remoteSort:false
});
if (!this.down('#grid')){
c.add({
xtype: 'rallygrid',
itemId: 'grid',
store: this._store,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'Defect Count', dataIndex: 'DefectCount'
},
{
text: 'Defects', dataIndex: 'Defects',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(defect){
html.push('' + defect.FormattedID + '')
});
return html.join(', ');
}
}
]
});
}
else{
this.down('#grid').reconfigure(this._store);
}
}
});

extjs4 ajax api store update

I'm trying to update my store and then database after using cellediting and a combobox in a gridpanel to update a record. The update operation.action in the ajax proxy is firing correctly, it just that the store and the grid aren't syncronizing, and the post tab in firebug says my json looks like this: 'data []'. How do I get the store record to create the json and update the record? Thanks for looking at this in advance...
Ext.Loader.setConfig({
enabled: true
});
Ext.Loader.setPath('Ext.ux', '/extjs4/examples/ux');
Ext.require([
'Ext.layout.container.Fit',
'Ext.grid.*',
'Ext.data.*',
'Ext.util.*',
'Ext.panel.*',
'Ext.selection.CellModel',
'Ext.state.*',
'Ext.form.*',
'Ext.ux.CheckColumn']);
Ext.define('Ext.app.HirePlanGrid', {
extend: 'Ext.panel.Panel',
alias: 'widget.hirePlangrid',
hireplanstoreId: 'hireplanstore',
hiremonthstoreId: 'hiremonthstore'
,
renderMonth: function (value, p, record) {
var fkStore = Ext.getStore(this.up('hirePlangrid').hiremonthstoreId);
var rec = fkStore.findRecord("MONTH_ID", value);
//return rec.get("ABBREVIATED_MONTH");
}
,
initComponent: function () {
var rIdx = '';
var cIdx = '';
this.editing = Ext.create('Ext.grid.plugin.CellEditing', {
clicksToEdit: 1,
listeners: {
'beforeedit': function (e) {
var me = this;
var allowed = !! me.isEditAllowed;
if (!me.isEditAllowed) Ext.Msg.confirm('confirm', 'Are you sure?', function (btn) {
if (btn !== 'yes') return;
me.isEditAllowed = true;
me.startEditByPosition({
row: e.rowIdx,
column: e.colIdx
});
});
rIdx = e.rowIdx;
cIdx = e.colIdx;
// alert('rIdx= ' + rIdx + ' cIdx = ' + cIdx);
return allowed;
},
'edit': function (e) {
this.isEditAllowed = true;
}
}
});
var objMonthStore = Ext.getStore(this.hiremonthstoreId);
objMonthStore.load();
var objStore = Ext.getStore(this.hireplanstoreId);
objStore.setProxy({
type: 'ajax',
url: 'hireplan.cfc?method=getEmployees'
});
objStore.load();
var onDeleteClick = function (field, value, options) {
// var objPanel = this.down('gridpanel');
var selection = Ext.getCmp('grid').getSelectionModel().getSelection();
// alert(selection);
//var selection = getView().getSelectionModel().getSelection()[r];
if (value) {
//alert(value);
objStore.remove(value);
objStore.sync();
}
};
var onUpdateClick = function (field, value, options) {
alert('field= ' + field + ' value= ' + value + 'options= ' + options);
objStore.update(this.hireplanstoreId, value, 'update', options);
onSync();
};
var onSync = function () {
objStore.sync();
};
Ext.apply(this, {
layout: 'fit',
width: 800,
//height: 1500,
items: [{
xtype: 'grid',
id: 'gridgrid',
//height: 300,
store: objStore,
selModel: {
selType: 'cellmodel'
},
selType: 'rowmodel',
plugins: [this.editing],
// plugins: [cellEditing],
columnLines: true,
viewConfig: {
stripeRows: true
},
//loadMask: true,
disableSelection: true,
columns: [{
header: 'rowid',
hidden: true,
dataIndex: 'ROWID'
}, {
header: 'Indicator',
id: 'chkcolumn',
xtype: 'checkcolumn',
dataIndex: 'CHK_COL',
editor: {
xtype: 'checkbox',
cls: 'x-grid-checkheader-editor'
},
listeners: {
checkchange: function (column, recordIndex, checked) {
alert('checked rindex= ' + recordIndex);
onDeleteClick(column, recordIndex, checked);
//or send a request
}
}
}, {
id: 'employeeid',
header: 'employeeid',
width: 80,
hidden: false,
sortable: true,
dataIndex: 'EMPLOYEEID',
flex: 1
}, {
id: 'NATIONALIDNUMBER',
header: 'NATIONALIDNUMBER',
width: 80,
sortable: true,
dataIndex: 'NATIONALIDNUMBER',
flex: 1
}, {
id: 'MARITALSTATUS',
header: 'MARITALSTATUS',
width: 80,
sortable: true,
dataIndex: 'MARITALSTATUS',
flex: 1
}, {
id: 'PotentialforInsourcingKV',
header: 'Potential for Insourcing',
width: 30,
sortable: true,
dataIndex: 'POTENTIAL_FOR_INSOURCING',
flex: 1,
editor: {
id: 'thiscombo',
xtype: 'combobox',
typeAhead: true,
triggerAction: 'all',
selectOnTab: true,
store: [
['1', 'Yes'],
['0', 'No']
],
lazyRender: true,
listClass: 'x-combo-list-small',
listeners: {
scope: this,
'select': function () {
var selval = Ext.getCmp('thiscombo').getValue();
var row = rIdx;
//alert(selval + ' ' + rIdx);
onUpdateClick('thiscombo', rIdx, selval);
}
}
}
}, {
id: 'ABBREVIATED_MONTH',
header: 'ABBREVIATED_MONTH',
width: 80,
sortable: true,
dataIndex: 'ABBREVIATED_MONTH',
flex: 1,
renderer: this.renderMonth,
field: {
xtype: 'combobox',
store: Ext.getStore(this.hiremonthstoreId),
typeAhead: true,
lazyRender: true,
queryMode: 'local',
displayField: 'ABBREVIATED_MONTH',
valueField: 'MONTH_ID',
listClass: 'x-combo-list-small'
}
}, {
id: 'SALARIEDFLAG',
header: 'SALARIEDFLAG',
width: 80,
sortable: true,
dataIndex: 'SALARIEDFLAG',
flex: 1
}],
features: [{
ftype: 'rowbody'
}]
}]
});
this.callParent(arguments);
}, //initComponent
onSelectChange: function (selModel, selections) {
this.down('#delete').setDisabled(selections.length === 0);
},
viewConfig: {},
});
// JavaScript Document
// JavaScript Document
hireplanstore = Ext.create("Ext.data.Store", {
model: 'HiringPlan',
//autoLoad: true,
//autoSync: true,
buffered: true,
storeId: 'hireplanstore',
remoteFilter: true
,
proxy: {
type: 'ajax',
simpleSortMode: true,
api: {
read: 'hireplan.cfc?method=GetEmployees',
update: 'hireplan.cfc?method=upEmployees',
destroy: 'hireplan.cfc?method=delEmployees'
},
reader: {
type: 'json',
messageProperty: 'message',
successProperty: 'success',
root: 'data'
},
writer: {
type: 'json',
writeAllFields: false,
root: 'data'
},
listeners: {
exception: function (proxy, response, operation) {
Ext.MessageBox.show({
title: 'ERROR from store',
msg: operation.getError(),
icon: Ext.MessageBox.ERROR,
buttons: Ext.Msg.OK
});
}
}
}
});
//hireplanstore.pageSize = 10000;
Ext.define('HiringPlan', {
extend: 'Ext.data.Model',
fields: [{
name: 'ROWID',
type: 'string'
}, {
name: 'EMPLOYEEID',
type: 'string'
}, {
name: 'NATIONALIDNUMBER',
type: 'string'
}, {
name: 'MARITALSTATUS',
type: 'string'
}, {
name: 'GENDER',
type: 'string'
}, {
name: 'POTENTIAL_FOR_INSOURCING',
type: 'integer'
}, {
name: 'ABBREVIATED_MONTH',
type: 'string'
}, {
name: 'SALARIEDFLAG',
type: 'string'
}, {
name: 'CHK_COL',
type: 'bool'
}]
});
In order to update correctly your ajax or rest call have to return an array containing the updated records, even if it's a single record, you have to return it inside an array, a sample json response (for the rest proxy) should be like this:
[{'id': 1, 'name': 'test', 'foo': 'bar'}]