diff --git a/App-debug.html b/App-debug.html
index 60d91ee..261e870 100644
--- a/App-debug.html
+++ b/App-debug.html
@@ -17,7 +17,7 @@
Rally.launchApp('CustomApp', {
name:"portfolio-item-copy",
parentRepos:"",
- version:"1.0.1"
+ version:"1.0.2"
});
}, true);
diff --git a/App.js b/App.js
index e0e7ab6..76ae6dc 100644
--- a/App.js
+++ b/App.js
@@ -12,7 +12,8 @@ Ext.define('CustomApp', {
defaultSettings: {
portfolioitem : [''],
hierarchicalrequirement : ["ScheduleState","PlanEstimate"],
- task : ["State","Estimate","TaskIndex","ToDo","Actuals"]
+ task : ["State","Estimate","TaskIndex","ToDo","Actuals"],
+ preserve_rank: true,
}
},
@@ -103,7 +104,9 @@ Ext.define('CustomApp', {
success: function(requiredFields) {
app.requiredFields = requiredFields;
// make sure required fields are part of the settings:
- Ext.apply(app.fieldsToCopy, requiredFields);
+ _.each(_.keys(app.fieldsToCopy), function(type) {
+ app.fieldsToCopy[type] = _.uniq(app.fieldsToCopy[type].concat(requiredFields[type]));
+ });
if ( Ext.Array.contains( requiredFields.hierarchicalrequirement, 'Release' )) {
this.down('#release-label').setText('Default Release:');
@@ -134,7 +137,7 @@ Ext.define('CustomApp', {
title: 'Choose Item',
listeners: {
artifactChosen: function(dialog, selectedRecord){
- this.down("#item-label").setText(selectedRecord.get('Name'));
+ this.down("#item-label").setText(selectedRecord.get('FormattedID') + ' - ' + selectedRecord.get('Name'));
this.down("#copy-button").setDisabled(true);
app.itemSelected(selectedRecord);
@@ -148,6 +151,8 @@ Ext.define('CustomApp', {
// updates the summary message.
itemSelected : function(root) {
+ app.setLoading('Analyzing hierarchy...');
+
var config = { model : "PortfolioItem",
fetch : true,
filters : [ { property : "ObjectID", operator : "=", value: root.get("ObjectID") } ]
@@ -174,8 +179,11 @@ Ext.define('CustomApp', {
// check project selected before enabling.
var projectRef = app.down("#project-picker").getValue();
- if (projectRef !== null && projectRef !== "")
+ if (projectRef !== null && projectRef !== "") {
app.down("#copy-button").setDisabled(false);
+ }
+
+ app.setLoading(false);
});
});
});
@@ -282,7 +290,8 @@ Ext.define('CustomApp', {
callback: function(result, operation) {
if (operation.success===true) {
app.copyList[item.source.get("_ref")] = result.get("_ref");
- app.down("#summary").setText("Created " + result.get("FormattedID"));
+ app.down("#summary").setText('(' + _.keys(app.copyList).length + ' of ' + app.list.length + '): ' +
+ "Created " + result.get("FormattedID") + ' - ' + result.get("Name"));
callback(null,result);
} else {
console.log("Error:",operation);
@@ -299,7 +308,13 @@ Ext.define('CustomApp', {
// reads a rally collection object
readCollection : function( collectionConfig, callback ) {
- collectionConfig.reference.getCollection(collectionConfig.type,{fetch:true}).load({
+ var rank_field = Rally.data.Ranker.getRankField(collectionConfig.reference);
+ var direction = collectionConfig.direction;
+
+ collectionConfig.reference.getCollection(collectionConfig.type, {
+ fetch: true,
+ sorters: [ { property: rank_field, direction: direction} ]
+ }).load({
fetch : true,
callback : function(records,operation,success) {
callback(null,records);
@@ -311,7 +326,8 @@ Ext.define('CustomApp', {
// recursive method to create a list of all items to be copied.
createList : function(root,callback) {
- var config = { model : root.raw._type,
+ var config = {
+ model : root.raw._type,
fetch : true,
filters : [ { property : "ObjectID", operator : "=", value: root.get("ObjectID") } ]
};
@@ -319,13 +335,18 @@ Ext.define('CustomApp', {
async.map([config], wsapiQuery, function(err,results) {
var obj = results[0][0];
+ var direction = "DESC";
app.list.push(obj);
var childRef = null;
if (app.defined(obj.get("Tasks"))) {
childRef = "Tasks";
+ direction = "DESC";
} else {
- if (app.defined(obj.get("Children"))){
+ if (app.defined(obj.get("Children"))) {
childRef = "Children";
+ if (/PortfolioItem/.test(root.raw._type)) {
+ direction = "ASC";
+ }
} else {
if (app.defined(obj.get("UserStories"))) {
childRef = "UserStories";
@@ -334,13 +355,22 @@ Ext.define('CustomApp', {
}
if (app.isObject(childRef)) {
- var config = { reference : obj, type : childRef };
- async.map([config],app.readCollection,function(err,results){
- var children = results[0];
- async.map(children,app.createList,function(err,results){
- callback(null,results);
+ var config = { reference : obj, type : childRef, direction: direction };
+ if ( app.getSetting('preserve_rank') && app.getSetting('preserve_rank') != "false" ) {
+ async.mapSeries([config],app.readCollection,function(err,results){
+ var children = results[0];
+ async.mapSeries(children,app.createList,function(err,results){
+ callback(null,results);
+ });
});
- });
+ } else {
+ async.map([config],app.readCollection,function(err,results){
+ var children = results[0];
+ async.map(children,app.createList,function(err,results){
+ callback(null,results);
+ });
+ });
+ }
} else {
callback(null,obj);
}
@@ -394,6 +424,7 @@ Ext.define('CustomApp', {
model : config.model,
fetch : config.fetch,
filters : config.filters,
+ sorters: config.sorters,
listeners : {
scope : this,
load : function(store, data) {
@@ -442,14 +473,21 @@ Ext.define('CustomApp', {
},
getSettingsFields: function() {
- return [{
+ return [
+ {
+ name: 'preserve_rank',
+ xtype: 'rallycheckboxfield',
+ margin: '10px 0 10px 0',
+ fieldLabel: 'Maintain Ranks'
+ },
+ {
name: 'portfolioitem',
xtype: 'rallyfieldpicker',
autoExpand: true,
alwaysExpanded: false,
- modelTypes: ['PortfolioItem/Feature'],
+ modelTypes: ['PortfolioItem'],
margin: '10px 0 10px 0',
- fieldLabel: 'Feature Fields',
+ fieldLabel: 'Portfolio Item Fields',
_shouldShowField: function(field) {
//console.log(field.name, field);
var attr = field.attributeDefinition;
diff --git a/README.md b/README.md
index b3085d7..59d49cf 100644
--- a/README.md
+++ b/README.md
@@ -14,14 +14,30 @@ By default, it copies the following fields:
If there are required fields, the app will attempt to copy those fields, as well. If the Release field is required for stories, the app will present the user with a drop-down box for default release to put in as a placeholder for any parent stories that are being copied. (A parent story cannot have a release, so if it's required, a parent being copied will fail to copy because the story is first created as a standalone and then becomes a parent when a child is assigned to it.)
-In addition, the app can be configured to copy additional fields by using Edit App Settings...
+![](images/screenshot.png)
-![alt text](https://raw.githubusercontent.com/wrackzone/portfolio-item-copy/master/screenshot.png "Screenshot")
+## Settings
+This app can be configured via the Edit App Settings gear menu item.
+
+![](images/settings.png)
+
+### Maintain Ranks
+
+This allows for the rank order to be maintained in the copy. For large trees, this could be slower. (default: true)
+
+### Feature Fields
+
+Use to set additional fields from PortfolioItems to copy beyond the default and required fields.
+
+### Story Fields
+
+Use to set additional fields from User Stories to copy beyond the default and required fields.
+
+### Task Fields
+
+Use to set additional fields from Tasks to copy beyond the default and required fields.
-## License
-AppTemplate is released under the MIT license. See the file [LICENSE](./LICENSE) for the full text.
-##Documentation for SDK
diff --git a/deploy/App-external.html b/deploy/App-external.html
index 2bf95ec..e23c1b5 100644
--- a/deploy/App-external.html
+++ b/deploy/App-external.html
@@ -8,13 +8,13 @@