I am getting the hang of 2.0, but a bit stuck on something that seems simple.
Basically, I have created a new app for my team to use (thanks all for the help). I thought it would be cool to have a way I could add messages to the dashboard.
I decided the easiest way to accomplish this, is to create a story and in my code simply query that one story, grab the description and show it in the app. Sounds easy enough right?
I am having a bit of a time simple running out grabbing the Description field and showing it. I know it sounds odd, but it seems so complicated. I have tried this way
showMessage: function (message) {
debugger;
this.add({
xtype: 'label',
html: message
});
},
getMessage: function () {
var defectStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
fetch: ['Description'],
filters: [{
property: 'FormattedID',
operator: '=',
value: 'US13258'
}],
autoLoad: true,
listeners: {
load: function (store, records) {
debugger;
if (records)
return records[0].get("Description");
}
}
});
},
But seems to get caught up in event spaghetti. Surely there is an easier way :)
Just want to go grab a specific stories description field...
You can use a model's load method to do this:
var storyOid = 12345;
//Get the story model
Rally.data.ModelFactory.getModel({
type: 'UserStory',
success: function(model) {
//Load the specific story
model.load(storyOid, {
fetch: ['Description']
success: function(record) {
//success!
var description = record.get('Description');
}
});
}
});
I'm not sure why you're trying to do it with a listener, but I would just call load and get the result upon success, like so:
getMessage: function (storyID) {
var defectStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
fetch: ['Description'],
filters: [{
property: 'FormattedID',
operator: '=',
value: storyID
}],
autoLoad: true
});
defectStore.load({scope: this, callback: function(records, operation, success) {
if(success){
console.log(records[0].get('Description')); // additional logic here
} else {
console.log('You ruined the store. Jerk.');
}
}});
}
I'm thinking you might have some issues, though, unless call showMessage after you check for success, as extJS operates asynchronously.
Related
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
});
// ...
I want to present the user with a checkbox list of portfolio item features which are currently assigned to a particular release.
The rallymultiobjectpicker with modelType set to portfolioitem/feature looks perfect for this, but it is unclear to me how to filter the objects that it displays.
Presumably this is in the storeConfig parameter? I've tried the following to no effect:
{
xtype: 'rallymultiobjectpicker',
modelType: 'portfolioitem/feature',
fieldLabel: 'Select Features',
storeConfig: {
filters: [{
property: 'Release.Name',
operator: '=',
value: myRelease
}]
},
}
As a workaround until the bug is fixed, you might try filtering on the load, like this:
{
xtype: 'rallymultiobjectpicker',
modelType: 'portfolioitem/feature',
fieldLabel: 'Select Features',
storeConfig: {
fetch: ['Release','Name']
},
storeLoadOptions: function(records) {
var store = this;
Ext.Array.each(store.getRecords(), function(record,idx){
var release = record.get('Release');
var name = null;
if ( !release || release.Name !== myRelease ) {
store.remove(record);
}
});
}
}
It's going to be slower because it goes and gets all the features first, but it seems to work.
I have been trying to query Rally just to get a certain object by its ObjectID, but then I end up needing its parent in many cases. For example, for a task, I need its associated User Story, and that Story's Feature. It ended up being quite the cascade of callbacks (fair warning, it's ugly) - can anyone recommend a more efficient solution? The ability to query by OID is nice, but its too bad I need more than just information about that OID. (Note - solution must utilize WSAPI, not LBAPI).
Rally.data.WsapiModelFactory.getModel({
type: 'Task',
context: {
workspace: Rally.util.Ref.getRelativeUri()
},
success: function(taskModel) {
taskModel.load(oid, {
scope: this,
callback: function(taskRecord, op, success) {
if (taskRecord && taskRecord.data.WorkProduct && taskRecord.data.WorkProduct._type == "HierarchicalRequirement") {
// get User Story
Rally.data.WsapiModelFactory.getModel({
type: 'User Story',
context: {
workspace: Rally.util.Ref.getRelativeUri()
},
success: function(userStoryModel) {
userStoryModel.load(taskRecord.data.WorkProduct._ref, {
scope: this,
callback: function(storyRecord, op, success) {
if (storyRecord && storyRecord.data && storyRecord.data.Feature) {
// Get Feature
Rally.data.WsapiModelFactory.getModel({
type: 'PortfolioItem/Feature',
context: {
workspace: Rally.util.Ref.getRelativeUri()
},
success: function(featureModel) {
featureModel.load(storyRecord.data.Feature._ref, {
scope: this,
callback: function(featureRecord) {
displayTask(oid, taskRecord, storyRecord, featureRecord);
}
});
}
});
}
}
});
}
});
}
}
});
}
});
You can pull in the Work Product parent and its associated Feature directly in a single query. Try this:
Ext.create('Rally.data.WsapiDataStore', {
model : 'Task',
fetch : ['WorkProduct','Name','Feature'],
filters : [{
property : 'ObjectID',
value : OID
}]
}).load({
callback : function(records, operation, success) {
var task = records[0];
var userStory = task.get('WorkProduct');
var feature = userStory.Feature;
}
});
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.
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>'
]
}]
});