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 @@