From c7b122052c5e3fb53a73e0377b1cde34881569f0 Mon Sep 17 00:00:00 2001 From: pcw216 Date: Sat, 12 Jul 2014 14:04:21 -0400 Subject: [PATCH 1/5] Porting RUI services and utilities to angular app sdk --- src/rally/api/index.coffee | 3 + src/rally/api/services/index.coffee | 5 ++ src/rally/api/services/lookback.coffee | 21 +++++++ src/rally/api/services/slm.coffee | 14 +++++ src/rally/api/services/wsapi.coffee | 43 +++++++++++++ .../app/iframe/decorators/rootScope.coffee | 0 .../app/iframe/index.coffee | 0 .../iframe/services/iframeAppService.coffee | 0 .../iframe/services/iframeMessageBus.coffee | 0 .../app/iframe/services/index.coffee | 0 src/rally/app/index.coffee | 3 + src/rally/app/services/index.coffee | 3 + .../app}/services/rally.coffee | 5 +- .../directives/component-directive.coffee | 0 src/{scripts => rally}/index.coffee | 6 +- src/rally/util/async/index.coffee | 1 + src/rally/util/cache/factories/wrap.coffee | 30 +++++++++ src/rally/util/cache/index.coffee | 3 + .../util/http/factories/httpWrapper.coffee | 24 +++++++ src/rally/util/http/index.coffee | 3 + src/rally/util/lodash/index.coffee | 3 + src/rally/util/lodash/sortedInsert.coffee | 8 +++ src/rally/util/timeout/index.coffee | 25 ++++++++ src/rally/util/util.coffee | 7 +++ src/scripts/app/index.coffee | 1 - src/scripts/services/index.coffee | 1 - test/api/services/lookbackSpec.coffee | 15 +++++ test/api/services/wsapiSpec.coffee | 13 ++++ .../http/factories/httpWrapperSpec.coffee | 25 ++++++++ test/util/timeout/indexSpec.coffee | 63 +++++++++++++++++++ 30 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 src/rally/api/index.coffee create mode 100644 src/rally/api/services/index.coffee create mode 100644 src/rally/api/services/lookback.coffee create mode 100644 src/rally/api/services/slm.coffee create mode 100644 src/rally/api/services/wsapi.coffee rename src/{scripts => rally}/app/iframe/decorators/rootScope.coffee (100%) rename src/{scripts => rally}/app/iframe/index.coffee (100%) rename src/{scripts => rally}/app/iframe/services/iframeAppService.coffee (100%) rename src/{scripts => rally}/app/iframe/services/iframeMessageBus.coffee (100%) rename src/{scripts => rally}/app/iframe/services/index.coffee (100%) create mode 100644 src/rally/app/index.coffee create mode 100644 src/rally/app/services/index.coffee rename src/{scripts => rally/app}/services/rally.coffee (90%) rename src/{scripts => rally}/directives/component-directive.coffee (100%) rename src/{scripts => rally}/index.coffee (64%) create mode 100644 src/rally/util/async/index.coffee create mode 100644 src/rally/util/cache/factories/wrap.coffee create mode 100644 src/rally/util/cache/index.coffee create mode 100644 src/rally/util/http/factories/httpWrapper.coffee create mode 100644 src/rally/util/http/index.coffee create mode 100644 src/rally/util/lodash/index.coffee create mode 100644 src/rally/util/lodash/sortedInsert.coffee create mode 100644 src/rally/util/timeout/index.coffee create mode 100644 src/rally/util/util.coffee delete mode 100644 src/scripts/app/index.coffee delete mode 100644 src/scripts/services/index.coffee create mode 100644 test/api/services/lookbackSpec.coffee create mode 100644 test/api/services/wsapiSpec.coffee create mode 100644 test/util/http/factories/httpWrapperSpec.coffee create mode 100644 test/util/timeout/indexSpec.coffee diff --git a/src/rally/api/index.coffee b/src/rally/api/index.coffee new file mode 100644 index 0000000..c2c8f12 --- /dev/null +++ b/src/rally/api/index.coffee @@ -0,0 +1,3 @@ +angular.module('rally.api', [ + 'rally.api.services' +]) diff --git a/src/rally/api/services/index.coffee b/src/rally/api/services/index.coffee new file mode 100644 index 0000000..dd7fcaf --- /dev/null +++ b/src/rally/api/services/index.coffee @@ -0,0 +1,5 @@ +angular.module('rally.api.services',[ + 'rally.api.services.slm' + 'rally.api.services.wsapi' + 'rally.api.services.lookback' +]) diff --git a/src/rally/api/services/lookback.coffee b/src/rally/api/services/lookback.coffee new file mode 100644 index 0000000..3e2c3e5 --- /dev/null +++ b/src/rally/api/services/lookback.coffee @@ -0,0 +1,21 @@ +###* + * @ngdoc service + * @name rally.api.services:$lookback + * @description + * @example +### +angular.module('rally.api.services.lookback', [ +]).provider '$lookback', () -> + @baseUrl = '/analytics/v2.0/service/rally' + @setBaseUrl = (@baseUrl)=> + + class LookbackApi + constructor: (@$http, @baseUrl) -> + + artifactSnapshots: (workspaceOid, query, config) => + # Must use JSON.stringify, angular.toJson removes keys with $, which are kind of important in lookback queries :) + return @$http.post("#{@baseUrl}/workspace/#{workspaceOid}/artifact/snapshot/query", JSON.stringify(query), config) + + @$get = ($http) => + return new LookbackApi($http, @baseUrl) + return @ diff --git a/src/rally/api/services/slm.coffee b/src/rally/api/services/slm.coffee new file mode 100644 index 0000000..7961fb5 --- /dev/null +++ b/src/rally/api/services/slm.coffee @@ -0,0 +1,14 @@ +###* + * @ngdoc service + * @name rally.api.services:$slm + * @description + * @example +### +angular.module('rally.api.services.slm', [ + 'rally.util.http' +]).provider '$slm', ()-> + @baseUrl = '/slm/' + @setBaseUrl = (@baseUrl)=> + @$get = (rallyHttpWrapper)=> + return rallyHttpWrapper(@baseUrl) + return @ diff --git a/src/rally/api/services/wsapi.coffee b/src/rally/api/services/wsapi.coffee new file mode 100644 index 0000000..6315472 --- /dev/null +++ b/src/rally/api/services/wsapi.coffee @@ -0,0 +1,43 @@ +###* + * @ngdoc service + * @name rally.api.services:$wsapi + * @description + * Abstracts $http calls to a wsapi base url. Can be configured at config/provider time to use a particular base url. + * @example + + + angular.module('App', ['rally.api.services.wsapi']) + .config(function($wsapiProvider){ + $wsapiProvider.setBaseUrl('/test/'); + }) + .controller('Ctrl', + function Ctrl($scope, $httpBackend, $wsapi) { + $scope.$wsapi = $wsapi; + console.log('backend', $httpBackend) + console.log('backend.whenGET', $httpBackend.whenGET) + $httpBackend.whenGET('/test/sub/url').respond(200, {foo: 'bar'}); + $wsapi({method: 'GET', url: '/sub/url/'}).then(function(data, status){ + $scope.data = data + $scope.status = status + }); + } + ); + + +
+
Wsapi base url: {{$wsapi.baseUrl}}
+
data: {{data}}
+
status: {{status}}
+
+
+
+### +angular.module('rally.api.services.wsapi', [ + 'rally.api.services.slm' + 'rally.util.http' +]).provider '$wsapi', ()-> + @baseUrl = '/webservice/v2.0/' + @setBaseUrl = (@baseUrl)=> + @$get = (rallyHttpWrapper, $slm)=> + return rallyHttpWrapper(@baseUrl, $slm) + return @ diff --git a/src/scripts/app/iframe/decorators/rootScope.coffee b/src/rally/app/iframe/decorators/rootScope.coffee similarity index 100% rename from src/scripts/app/iframe/decorators/rootScope.coffee rename to src/rally/app/iframe/decorators/rootScope.coffee diff --git a/src/scripts/app/iframe/index.coffee b/src/rally/app/iframe/index.coffee similarity index 100% rename from src/scripts/app/iframe/index.coffee rename to src/rally/app/iframe/index.coffee diff --git a/src/scripts/app/iframe/services/iframeAppService.coffee b/src/rally/app/iframe/services/iframeAppService.coffee similarity index 100% rename from src/scripts/app/iframe/services/iframeAppService.coffee rename to src/rally/app/iframe/services/iframeAppService.coffee diff --git a/src/scripts/app/iframe/services/iframeMessageBus.coffee b/src/rally/app/iframe/services/iframeMessageBus.coffee similarity index 100% rename from src/scripts/app/iframe/services/iframeMessageBus.coffee rename to src/rally/app/iframe/services/iframeMessageBus.coffee diff --git a/src/scripts/app/iframe/services/index.coffee b/src/rally/app/iframe/services/index.coffee similarity index 100% rename from src/scripts/app/iframe/services/index.coffee rename to src/rally/app/iframe/services/index.coffee diff --git a/src/rally/app/index.coffee b/src/rally/app/index.coffee new file mode 100644 index 0000000..cc6eefa --- /dev/null +++ b/src/rally/app/index.coffee @@ -0,0 +1,3 @@ +angular.module('rally.app', [ + 'rally.app.services' +]) diff --git a/src/rally/app/services/index.coffee b/src/rally/app/services/index.coffee new file mode 100644 index 0000000..a22673d --- /dev/null +++ b/src/rally/app/services/index.coffee @@ -0,0 +1,3 @@ +angular.module('rally.app.services', [ + 'rally.app.services.rally' +]) diff --git a/src/scripts/services/rally.coffee b/src/rally/app/services/rally.coffee similarity index 90% rename from src/scripts/services/rally.coffee rename to src/rally/app/services/rally.coffee index 43ff252..fe1fbe0 100644 --- a/src/scripts/services/rally.coffee +++ b/src/rally/app/services/rally.coffee @@ -1,6 +1,5 @@ -angular.module('rally.services.rally', ['Ext', 'Rally']).service '$rally', - - class RallyService +angular.module('rally.app.services.rally', ['Ext', 'Rally']).service '$rally', + class RallyAppService constructor: (@$q, @$rootScope, @$log, @Ext, @Rally) -> diff --git a/src/scripts/directives/component-directive.coffee b/src/rally/directives/component-directive.coffee similarity index 100% rename from src/scripts/directives/component-directive.coffee rename to src/rally/directives/component-directive.coffee diff --git a/src/scripts/index.coffee b/src/rally/index.coffee similarity index 64% rename from src/scripts/index.coffee rename to src/rally/index.coffee index 5dd6d97..263bfbf 100644 --- a/src/scripts/index.coffee +++ b/src/rally/index.coffee @@ -1,3 +1,7 @@ angular.module('Ext', []).factory('Ext', () -> return Ext ) angular.module('Rally', []).factory('Rally', () -> return Rally ) -angular.module 'rally', ['rally.services', 'rally.app'] +angular.module 'rally', [ + 'rally.api', + 'rally.app' + 'rally.util' +] diff --git a/src/rally/util/async/index.coffee b/src/rally/util/async/index.coffee new file mode 100644 index 0000000..1b13f71 --- /dev/null +++ b/src/rally/util/async/index.coffee @@ -0,0 +1 @@ +angular.module('rally.util.async', []).factory('async', ()-> return async) diff --git a/src/rally/util/cache/factories/wrap.coffee b/src/rally/util/cache/factories/wrap.coffee new file mode 100644 index 0000000..120a2f3 --- /dev/null +++ b/src/rally/util/cache/factories/wrap.coffee @@ -0,0 +1,30 @@ +###* + * @ngdoc service + * @name rally.util.cache:$cacheWrap + * @type function + * @description + * Helper function that returns a cached value or runs your function to cache the result and return your value as a promise. + * If you value function returns a promise, the wrapper will wait for resolution and put the result in your cache. +### +angular.module('rally.util.cache.factories.wrap', ['rally.util.lodash']).factory '$cacheWrap', ($q)-> + $cacheWrap = (cache, keyFn=_.identity) -> + return (key, func)-> + key = keyFn(key) + if cache.get(key) then return $q.when(cache.get(key)) + + # Run the function + deferred = $q.defer() + cache.put(key, deferred.promise) + + deferred.promise.then( + (result)-> + cache.put(key, result) + , + ()-> + cache.remove(key) + ) + + deferred.resolve($q.when(func())) + return deferred.promise + + return $cacheWrap diff --git a/src/rally/util/cache/index.coffee b/src/rally/util/cache/index.coffee new file mode 100644 index 0000000..6ae203e --- /dev/null +++ b/src/rally/util/cache/index.coffee @@ -0,0 +1,3 @@ +angular.module('rally.util.cache',[ + 'rally.util.cache.factories.wrap' +]) diff --git a/src/rally/util/http/factories/httpWrapper.coffee b/src/rally/util/http/factories/httpWrapper.coffee new file mode 100644 index 0000000..a6fabf9 --- /dev/null +++ b/src/rally/util/http/factories/httpWrapper.coffee @@ -0,0 +1,24 @@ +angular.module('rally.util.http.factories.httpWrapper', [ + 'rally.util.lodash' +]).factory 'rallyHttpWrapper', ($http)-> + return (baseUrl, http=$http)-> + getUrl = (url)-> + toAdd = url + if baseUrl[baseUrl.length - 1] is '/' and url.indexOf('/') is 0 + toAdd = toAdd.substring(1) + return baseUrl+toAdd + + wrapper = (urlOrConfig, config)-> + if _.isString(urlOrConfig) + urlOrConfig = getUrl(urlOrConfig) + else + urlOrConfig.url = getUrl(urlOrConfig.url) + http.call(this, urlOrConfig, config) + + # Write shortcut methods for GET/POST/PUT/DELETE/etc + _.each ['GET', 'DELETE', 'POST', 'PUT'], (method)-> + wrapper[method.toLowerCase()] = (url, args...)-> + url = getUrl(url) + http[method.toLowerCase()].call(@, url, args...) + + return wrapper diff --git a/src/rally/util/http/index.coffee b/src/rally/util/http/index.coffee new file mode 100644 index 0000000..3321061 --- /dev/null +++ b/src/rally/util/http/index.coffee @@ -0,0 +1,3 @@ +angular.module('rally.util.http', [ + 'rally.util.http.factories.httpWrapper' +]) diff --git a/src/rally/util/lodash/index.coffee b/src/rally/util/lodash/index.coffee new file mode 100644 index 0000000..8883786 --- /dev/null +++ b/src/rally/util/lodash/index.coffee @@ -0,0 +1,3 @@ +angular.module('rally.util.lodash', [ + 'rally.util.lodash.sortedInsert' +]) diff --git a/src/rally/util/lodash/sortedInsert.coffee b/src/rally/util/lodash/sortedInsert.coffee new file mode 100644 index 0000000..bb7afc8 --- /dev/null +++ b/src/rally/util/lodash/sortedInsert.coffee @@ -0,0 +1,8 @@ +angular.module('rally.util.lodash.sortedInsert', []).run -> + _.sortedInsert = (array, value, pluck)-> + index = _.sortedIndex( array, value, pluck ) + array.splice( index, 0, value ) + return array + _.sortedReverseInsert = (array, value, pluck)-> + _.sortedInsert(array.reverse(), value, pluck) + return array.reverse() diff --git a/src/rally/util/timeout/index.coffee b/src/rally/util/timeout/index.coffee new file mode 100644 index 0000000..b9bdb49 --- /dev/null +++ b/src/rally/util/timeout/index.coffee @@ -0,0 +1,25 @@ +# Throttles and queues timeouts so you don't overload the worker thread and block the browser. +angular.module('rally.util.timeout', []) +.factory '$rallyTimeoutThrottleFactory', ($timeout)-> + return (max)-> + queue = [] + running = 0 + + processNext = ()-> + if running >= max then return + next = queue.shift() + context = next[0] + args = next[1...] + running = running + 1 + $timeout.apply(context, args)['finally'](()-> + running = running - 1 + processNext() + + ) + return (args...)-> + queue.push([this].concat(args)) + processNext() + +# Default throttle processes 10 timeouts at a time. +.factory '$rallyTimeoutThrottle', ($rallyTimeoutThrottleFactory)-> + return $rallyTimeoutThrottleFactory(10) diff --git a/src/rally/util/util.coffee b/src/rally/util/util.coffee new file mode 100644 index 0000000..ef5520d --- /dev/null +++ b/src/rally/util/util.coffee @@ -0,0 +1,7 @@ +util = angular.module 'rally.util', [ + 'rally.util.async' + 'rally.util.cache' + 'rally.util.http' + 'rally.util.lodash' + 'rally.util.timeout' +] diff --git a/src/scripts/app/index.coffee b/src/scripts/app/index.coffee deleted file mode 100644 index d930bc9..0000000 --- a/src/scripts/app/index.coffee +++ /dev/null @@ -1 +0,0 @@ -angular.module('rally.app', []) \ No newline at end of file diff --git a/src/scripts/services/index.coffee b/src/scripts/services/index.coffee deleted file mode 100644 index eab0b62..0000000 --- a/src/scripts/services/index.coffee +++ /dev/null @@ -1 +0,0 @@ -angular.module('rally.services', ['rally.services.rally']) diff --git a/test/api/services/lookbackSpec.coffee b/test/api/services/lookbackSpec.coffee new file mode 100644 index 0000000..fa5e841 --- /dev/null +++ b/test/api/services/lookbackSpec.coffee @@ -0,0 +1,15 @@ +describe 'rally.api.services.lookback', -> + + describe '$lookbackProvider', -> + beforeEach -> + testModule = angular.module('test.rally.api.services.lookback', ['rally.api.services.lookback']).config (@$lookbackProvider) => + @$lookbackProvider.setBaseUrl('/analytics') + angular.mock.module('rally.api.services.lookback', 'test.rally.api.services.lookback') + + it 'should post a query to the lookback API', inject ($lookback, $httpBackend)-> + $httpBackend.expectPOST('/analytics/workspace/123/artifact/snapshot/query', (body) => + return body == '{"find":{"$pleaseDontFilterMe":"test"}}' + ).respond(200) + find = { $pleaseDontFilterMe: 'test' } + $lookback.artifactSnapshots(123, {find}) + $httpBackend.flush() diff --git a/test/api/services/wsapiSpec.coffee b/test/api/services/wsapiSpec.coffee new file mode 100644 index 0000000..67719ad --- /dev/null +++ b/test/api/services/wsapiSpec.coffee @@ -0,0 +1,13 @@ +describe 'rally.api.services.wsapi', -> + + describe '$wsapiProvider', -> + beforeEach -> + testModule = angular.module('test.rally.api.services.wsapi', ['rally.api.services.wsapi']).config (@$slmProvider, @$wsapiProvider) => + @$slmProvider.setBaseUrl('/slm/') + @$wsapiProvider.setBaseUrl('wsapi/') + angular.mock.module('rally.api.services.wsapi', 'test.rally.api.services.wsapi') + + it 'should append my request to the slm url', inject ($wsapi, $httpBackend)-> + $httpBackend.expectGET('/slm/wsapi/query').respond(200) + $wsapi.get('query') + $httpBackend.flush() diff --git a/test/util/http/factories/httpWrapperSpec.coffee b/test/util/http/factories/httpWrapperSpec.coffee new file mode 100644 index 0000000..465d058 --- /dev/null +++ b/test/util/http/factories/httpWrapperSpec.coffee @@ -0,0 +1,25 @@ +describe 'rally.util.http.factories.httpWrapper.rallyHttpWrapper', -> + + beforeEach angular.mock.module 'rally.util.http.factories.httpWrapper' + beforeEach inject (@rallyHttpWrapper, @$httpBackend)-> + + beforeEach -> + @composition = @rallyHttpWrapper('/baseUrl/') + afterEach -> + @$httpBackend.flush() + + it 'should append my request to the base url', -> + @$httpBackend.expectGET('/baseUrl/query').respond(200) + @composition({method: 'GET', url: 'query'}) + + it 'should remove double slashes', -> + @$httpBackend.expectGET('/baseUrl/query').respond(200) + @composition({method: 'GET', url: '/query'}) + + for method in ['GET','POST','PUT','DELETE'] + do (method) -> + it "should support the shortcut method #{method}", inject ($http)-> + spyOn($http, method.toLowerCase()).andCallThrough() + @$httpBackend.expect(method, '/baseUrl/query').respond(200) + @composition[method.toLowerCase()]('/query') + expect($http[method.toLowerCase()]).toHaveBeenCalled() diff --git a/test/util/timeout/indexSpec.coffee b/test/util/timeout/indexSpec.coffee new file mode 100644 index 0000000..97fba7b --- /dev/null +++ b/test/util/timeout/indexSpec.coffee @@ -0,0 +1,63 @@ +describe 'rally.util.timeout', -> + + beforeEach angular.mock.module('rally.util.timeout') + + describe '$rallyTimeoutThrottleFactory', -> + beforeEach -> + @$timeout = jasmine.createSpy('$timeout') + angular.module('test.rally.util.timeout', []).value('$timeout', @$timeout) + angular.mock.module('test.rally.util.timeout') + + beforeEach inject (@$rallyTimeoutThrottleFactory, @$q)-> + @$throttle = @$rallyTimeoutThrottleFactory(2) + + it 'should delegate to $timeout to run my function', -> + @$timeout.andReturn(@$q.when()) + spy = jasmine.createSpy('func') + @$throttle(spy, 'foo', 'bar') + expect(@$timeout).toHaveBeenCalledWith(spy, 'foo', 'bar') + + it 'should queue my timeouts', -> + spies = _.map([0,1,2], (i)-> jasmine.createSpy("throttled:#{i}")) + timeoutCalls = [] + @$timeout.andCallFake (args...)=> + deferred = @$q.defer() + timeoutCalls.push(deferred) + return deferred.promise + + _.each(spies, (spy)=>@$throttle(spy)) + expect(timeoutCalls.length).toEqual(2) + expect(@$timeout).toHaveBeenCalledWith(spies[0]) + expect(@$timeout).toHaveBeenCalledWith(spies[1]) + expect(@$timeout).not.toHaveBeenCalledWith(spies[2]) + + it 'should run timeouts when queue frees up', inject ($rootScope)-> + spies = _.map([0,1,2], (i)-> jasmine.createSpy("throttled:#{i}")) + timeoutCalls = [] + @$timeout.andCallFake (args...)=> + deferred = @$q.defer() + timeoutCalls.push(deferred) + return deferred.promise + + _.each(spies, (spy)=>@$throttle(spy)) + expect(timeoutCalls.length).toEqual(2) + expect(@$timeout).toHaveBeenCalledWith(spies[0]) + expect(@$timeout).toHaveBeenCalledWith(spies[1]) + expect(@$timeout).not.toHaveBeenCalledWith(spies[2]) + + # Resolve a queued timeout and + timeoutCalls[1].resolve() + $rootScope.$digest() + + expect(@$timeout).toHaveBeenCalledWith(spies[2]) + expect(timeoutCalls.length).toEqual(3) + + describe 'default $rallyTimeoutThrottle', -> + + beforeEach -> + @factorySpy = jasmine.createSpy() + angular.module('test.rally.util.timeout', []).value('$rallyTimeoutThrottleFactory', @factorySpy) + angular.mock.module('test.rally.util.timeout') + + it 'should create a timeout throttle with a default throttle value of 10', inject ($rallyTimeoutThrottle)-> + expect(@factorySpy).toHaveBeenCalledWith(10) From 498c1869f73e895a601f52756fc881317ffda090 Mon Sep 17 00:00:00 2001 From: pcw216 Date: Sat, 12 Jul 2014 14:05:34 -0400 Subject: [PATCH 2/5] consistency in test folder namespaces --- test/{ => rally}/api/services/lookbackSpec.coffee | 0 test/{ => rally}/api/services/wsapiSpec.coffee | 0 test/{ => rally}/app/iframe/decorators/rootScope_spec.coffee | 0 test/{ => rally}/app/iframe/services/iframeMessageBus_spec.coffee | 0 test/{ => rally}/util/http/factories/httpWrapperSpec.coffee | 0 test/{ => rally}/util/timeout/indexSpec.coffee | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => rally}/api/services/lookbackSpec.coffee (100%) rename test/{ => rally}/api/services/wsapiSpec.coffee (100%) rename test/{ => rally}/app/iframe/decorators/rootScope_spec.coffee (100%) rename test/{ => rally}/app/iframe/services/iframeMessageBus_spec.coffee (100%) rename test/{ => rally}/util/http/factories/httpWrapperSpec.coffee (100%) rename test/{ => rally}/util/timeout/indexSpec.coffee (100%) diff --git a/test/api/services/lookbackSpec.coffee b/test/rally/api/services/lookbackSpec.coffee similarity index 100% rename from test/api/services/lookbackSpec.coffee rename to test/rally/api/services/lookbackSpec.coffee diff --git a/test/api/services/wsapiSpec.coffee b/test/rally/api/services/wsapiSpec.coffee similarity index 100% rename from test/api/services/wsapiSpec.coffee rename to test/rally/api/services/wsapiSpec.coffee diff --git a/test/app/iframe/decorators/rootScope_spec.coffee b/test/rally/app/iframe/decorators/rootScope_spec.coffee similarity index 100% rename from test/app/iframe/decorators/rootScope_spec.coffee rename to test/rally/app/iframe/decorators/rootScope_spec.coffee diff --git a/test/app/iframe/services/iframeMessageBus_spec.coffee b/test/rally/app/iframe/services/iframeMessageBus_spec.coffee similarity index 100% rename from test/app/iframe/services/iframeMessageBus_spec.coffee rename to test/rally/app/iframe/services/iframeMessageBus_spec.coffee diff --git a/test/util/http/factories/httpWrapperSpec.coffee b/test/rally/util/http/factories/httpWrapperSpec.coffee similarity index 100% rename from test/util/http/factories/httpWrapperSpec.coffee rename to test/rally/util/http/factories/httpWrapperSpec.coffee diff --git a/test/util/timeout/indexSpec.coffee b/test/rally/util/timeout/indexSpec.coffee similarity index 100% rename from test/util/timeout/indexSpec.coffee rename to test/rally/util/timeout/indexSpec.coffee From a9f90334410b67d23a4de826f8360bce0f4920df Mon Sep 17 00:00:00 2001 From: pcw216 Date: Sat, 12 Jul 2014 14:12:53 -0400 Subject: [PATCH 3/5] Rebuild and some grunt/config changes due to folder moves --- Gruntfile.coffee | 16 +-- bower.json | 3 +- dist/rally.js | 276 +++++++++++++++++++++++++++++++++++++++++++---- karma.conf.js | 11 +- 4 files changed, 272 insertions(+), 34 deletions(-) diff --git a/Gruntfile.coffee b/Gruntfile.coffee index d9b9359..71fb634 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -13,7 +13,7 @@ module.exports = (grunt) -> copy: build: cwd: '<%= src %>/' - src: ['scripts/**/*.js', 'scripts/**/*.coffee', 'views/**/*'] + src: ['**/*.js', '**/*.coffee', '**/*.html', '**/*.jade'] dest: '<%= build %>/' expand: true @@ -22,7 +22,7 @@ module.exports = (grunt) -> dist: files: '<%= dist %>/rally.js': [ - '<%= build %>/scripts/**/*.js' + '<%= build %>/**/*.js' ] clean: @@ -35,9 +35,9 @@ module.exports = (grunt) -> bare: true expand: true flatten: false - cwd: '<%= build %>/scripts' + cwd: '<%= build %>' src: '**/*.coffee' - dest: '<%= build %>/scripts/' + dest: '<%= build %>' ext: '.js' # Compile all Jade templates @@ -45,8 +45,8 @@ module.exports = (grunt) -> views: files: [{ expand: true - cwd: '<%= build %>/views' - dest: '<%= build %>/views/' + cwd: '<%= build %>' + dest: '<%= build %>' ext: '.html' src: ["**/*.jade"] }] @@ -62,5 +62,5 @@ module.exports = (grunt) -> grunt.registerTask('build', ['clean', 'copy:build', 'coffee', 'jade:views']) grunt.registerTask('dist', ['build', 'concat:dist']) - grunt.registerTask('test', ['build', 'karma:unit']) - grunt.registerTask('default', ['build', 'test']) + grunt.registerTask('test', ['karma:unit']) + grunt.registerTask('default', ['test', 'dist']) diff --git a/bower.json b/bower.json index bdfbaec..ab6699d 100644 --- a/bower.json +++ b/bower.json @@ -5,7 +5,8 @@ "node_modules", "bower_components", "test", - "src" + "src", + "build" ], "dependencies": { "angular": "latest", diff --git a/dist/rally.js b/dist/rally.js index ca6c646..618fbf0 100644 --- a/dist/rally.js +++ b/dist/rally.js @@ -1,3 +1,109 @@ +angular.module('rally.api', ['rally.api.services']); + +angular.module('rally.api.services', ['rally.api.services.slm', 'rally.api.services.wsapi', 'rally.api.services.lookback']); + +/** + * @ngdoc service + * @name rally.api.services:$lookback + * @description + * @example +*/ + +var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +angular.module('rally.api.services.lookback', []).provider('$lookback', function() { + var LookbackApi, + _this = this; + this.baseUrl = '/analytics/v2.0/service/rally'; + this.setBaseUrl = function(baseUrl) { + _this.baseUrl = baseUrl; + }; + LookbackApi = (function() { + function LookbackApi($http, baseUrl) { + this.$http = $http; + this.baseUrl = baseUrl; + this.artifactSnapshots = __bind(this.artifactSnapshots, this); + } + + LookbackApi.prototype.artifactSnapshots = function(workspaceOid, query, config) { + return this.$http.post("" + this.baseUrl + "/workspace/" + workspaceOid + "/artifact/snapshot/query", JSON.stringify(query), config); + }; + + return LookbackApi; + + })(); + this.$get = function($http) { + return new LookbackApi($http, _this.baseUrl); + }; + return this; +}); + +/** + * @ngdoc service + * @name rally.api.services:$slm + * @description + * @example +*/ + +angular.module('rally.api.services.slm', ['rally.util.http']).provider('$slm', function() { + var _this = this; + this.baseUrl = '/slm/'; + this.setBaseUrl = function(baseUrl) { + _this.baseUrl = baseUrl; + }; + this.$get = function(rallyHttpWrapper) { + return rallyHttpWrapper(_this.baseUrl); + }; + return this; +}); + +/** + * @ngdoc service + * @name rally.api.services:$wsapi + * @description + * Abstracts $http calls to a wsapi base url. Can be configured at config/provider time to use a particular base url. + * @example + + + angular.module('App', ['rally.api.services.wsapi']) + .config(function($wsapiProvider){ + $wsapiProvider.setBaseUrl('/test/'); + }) + .controller('Ctrl', + function Ctrl($scope, $httpBackend, $wsapi) { + $scope.$wsapi = $wsapi; + console.log('backend', $httpBackend) + console.log('backend.whenGET', $httpBackend.whenGET) + $httpBackend.whenGET('/test/sub/url').respond(200, {foo: 'bar'}); + $wsapi({method: 'GET', url: '/sub/url/'}).then(function(data, status){ + $scope.data = data + $scope.status = status + }); + } + ); + + +
+
Wsapi base url: {{$wsapi.baseUrl}}
+
data: {{data}}
+
status: {{status}}
+
+
+
+*/ + +angular.module('rally.api.services.wsapi', ['rally.api.services.slm', 'rally.util.http']).provider('$wsapi', function() { + var _this = this; + this.baseUrl = '/webservice/v2.0/'; + this.setBaseUrl = function(baseUrl) { + _this.baseUrl = baseUrl; + }; + this.$get = function(rallyHttpWrapper, $slm) { + return rallyHttpWrapper(_this.baseUrl, $slm); + }; + return this; +}); + var __slice = [].slice; angular.module('rally.app.iframe.decorators.rootScope', []).config(function($provide) { @@ -79,28 +185,16 @@ angular.module('rally.app.iframe.services.messageBus', ['rally.app.iframe.decora angular.module('rally.app.iframe.services', ['rally.app.iframe.services.messageBus', 'rally.app.iframe.services.appService']); -angular.module('rally.app', []); - +angular.module('rally.app', ['rally.app.services']); +angular.module('rally.app.services', ['rally.app.services.rally']); -angular.module('Ext', []).factory('Ext', function() { - return Ext; -}); - -angular.module('Rally', []).factory('Rally', function() { - return Rally; -}); - -angular.module('rally', ['rally.services', 'rally.app']); - -angular.module('rally.services', ['rally.services.rally']); - -var RallyService, +var RallyAppService, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __slice = [].slice; -angular.module('rally.services.rally', ['Ext', 'Rally']).service('$rally', RallyService = (function() { - function RallyService($q, $rootScope, $log, Ext, Rally) { +angular.module('rally.app.services.rally', ['Ext', 'Rally']).service('$rally', RallyAppService = (function() { + function RallyAppService($q, $rootScope, $log, Ext, Rally) { this.$q = $q; this.$rootScope = $rootScope; this.$log = $log; @@ -110,7 +204,7 @@ angular.module('rally.services.rally', ['Ext', 'Rally']).service('$rally', Rally this.launchApp = __bind(this.launchApp, this); } - RallyService.prototype.launchApp = function(appName, options, scope) { + RallyAppService.prototype.launchApp = function(appName, options, scope) { var defaults, deferred, self, theApp; if (options == null) { options = {}; @@ -137,7 +231,7 @@ angular.module('rally.services.rally', ['Ext', 'Rally']).service('$rally', Rally return deferred.promise; }; - RallyService.prototype.bind = function(scope, observable) { + RallyAppService.prototype.bind = function(scope, observable) { var _this = this; this.$log.debug('Binding to Rally app events'); Ext.util.Observable.capture(observable, function() { @@ -152,6 +246,148 @@ angular.module('rally.services.rally', ['Ext', 'Rally']).service('$rally', Rally return observable; }; - return RallyService; + return RallyAppService; })()); + + + +angular.module('Ext', []).factory('Ext', function() { + return Ext; +}); + +angular.module('Rally', []).factory('Rally', function() { + return Rally; +}); + +angular.module('rally', ['rally.api', 'rally.app', 'rally.util']); + +angular.module('rally.util.async', []).factory('async', function() { + return async; +}); + +/** + * @ngdoc service + * @name rally.util.cache:$cacheWrap + * @type function + * @description + * Helper function that returns a cached value or runs your function to cache the result and return your value as a promise. + * If you value function returns a promise, the wrapper will wait for resolution and put the result in your cache. +*/ + +angular.module('rally.util.cache.factories.wrap', ['rally.util.lodash']).factory('$cacheWrap', function($q) { + var $cacheWrap; + $cacheWrap = function(cache, keyFn) { + if (keyFn == null) { + keyFn = _.identity; + } + return function(key, func) { + var deferred; + key = keyFn(key); + if (cache.get(key)) { + return $q.when(cache.get(key)); + } + deferred = $q.defer(); + cache.put(key, deferred.promise); + deferred.promise.then(function(result) { + return cache.put(key, result); + }, function() { + return cache.remove(key); + }); + deferred.resolve($q.when(func())); + return deferred.promise; + }; + }; + return $cacheWrap; +}); + +angular.module('rally.util.cache', ['rally.util.cache.factories.wrap']); + +var __slice = [].slice; + +angular.module('rally.util.http.factories.httpWrapper', ['rally.util.lodash']).factory('rallyHttpWrapper', function($http) { + return function(baseUrl, http) { + var getUrl, wrapper; + if (http == null) { + http = $http; + } + getUrl = function(url) { + var toAdd; + toAdd = url; + if (baseUrl[baseUrl.length - 1] === '/' && url.indexOf('/') === 0) { + toAdd = toAdd.substring(1); + } + return baseUrl + toAdd; + }; + wrapper = function(urlOrConfig, config) { + if (_.isString(urlOrConfig)) { + urlOrConfig = getUrl(urlOrConfig); + } else { + urlOrConfig.url = getUrl(urlOrConfig.url); + } + return http.call(this, urlOrConfig, config); + }; + _.each(['GET', 'DELETE', 'POST', 'PUT'], function(method) { + return wrapper[method.toLowerCase()] = function() { + var args, url, _ref; + url = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + url = getUrl(url); + return (_ref = http[method.toLowerCase()]).call.apply(_ref, [this, url].concat(__slice.call(args))); + }; + }); + return wrapper; + }; +}); + +angular.module('rally.util.http', ['rally.util.http.factories.httpWrapper']); + +angular.module('rally.util.lodash', ['rally.util.lodash.sortedInsert']); + +angular.module('rally.util.lodash.sortedInsert', []).run(function() { + _.sortedInsert = function(array, value, pluck) { + var index; + index = _.sortedIndex(array, value, pluck); + array.splice(index, 0, value); + return array; + }; + return _.sortedReverseInsert = function(array, value, pluck) { + _.sortedInsert(array.reverse(), value, pluck); + return array.reverse(); + }; +}); + +var __slice = [].slice; + +angular.module('rally.util.timeout', []).factory('$rallyTimeoutThrottleFactory', function($timeout) { + return function(max) { + var processNext, queue, running; + queue = []; + running = 0; + processNext = function() { + var args, context, next; + if (running >= max) { + return; + } + next = queue.shift(); + context = next[0]; + args = next.slice(1); + running = running + 1; + return $timeout.apply(context, args)['finally'](function() { + running = running - 1; + return processNext(); + }); + }; + return function() { + var args; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + queue.push([this].concat(args)); + return processNext(); + }; + }; +}).factory('$rallyTimeoutThrottle', function($rallyTimeoutThrottleFactory) { + return $rallyTimeoutThrottleFactory(10); +}); + +var util; + +util = angular.module('rally.util', ['rally.util.async', 'rally.util.cache', 'rally.util.http', 'rally.util.lodash', 'rally.util.timeout']); diff --git a/karma.conf.js b/karma.conf.js index b6bd609..a3dae36 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,13 +9,14 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ - 'bower_components/angular/angular.js', + 'bower_components/lodash/dist/lodash.min.js', + 'bower_components/angular/angular.min.js', 'bower_components/angular-mocks/angular-mocks.js', 'bower_components/jasmine.async/lib/jasmine.async.js', - 'src/scripts/**/*.js', - 'src/scripts/**/*.coffee', - 'src/views/**/*.html', + 'src/**/*.js', + 'src/**/*.coffee', + 'src/**/*.html', 'test/**/*.js', 'test/**/*.coffee' ], @@ -26,7 +27,7 @@ module.exports = function(config) { }, ngHtml2JsPreprocessor: { - stripPrefix: '<%= src %>/views/', + stripPrefix: '<%= src %>/', prependPredix: 'templates/', moduleName: 'rally.templates' }, From d4248406d81ed8ec50ab6f02e305a585d39338bf Mon Sep 17 00:00:00 2001 From: pcw216 Date: Sat, 12 Jul 2014 14:21:35 -0400 Subject: [PATCH 4/5] Strip much of the junk from the readme --- README.md | 145 ++---------------------------------------------------- 1 file changed, 3 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 93d66ec..f45c177 100644 --- a/README.md +++ b/README.md @@ -7,81 +7,7 @@ adapter, the primary purpose is to support the patterns and style of usage obser TODO ---- - -- [ ] Build distribution script -- [ ] Bower file with version - -Use Angular in ExtJS -==================== - -We've extended ```Ext.Component``` to bootstrap an angular module anywhere within your ext app. -The component will very simple call [angular.bootstrap](http://docs.angularjs.org/api/angular.bootstrap) -on the component's element after it renders and set up event bubbling between Ext and Angular. - -```javascript -template = new Ext.XTemplate('
'); -Ext.create('Angular.Component', { - module: 'myModule', - renderTo: 'angular-container', - renderTpl: template -}) -``` - -Use ExtJS in Angular -==================== - -Attributes and Configs -------- -Ext components and widgets are created in code, but we want to leverage Angular's support for attributes. -While angular makes it easy enough to define interpolated, evaluated, and isolate scope bound attributes, -it's not simple to do this dynamically nor where the value type is unknown. - -The value of all attributes (except explicitly stated special ones) will be passed as a part of the config object -to Ext.create(). - -### Interpolation -This is the default behavior for config attributes. Standard attributes will be compiled and evaluated as strings. -``` - -``` - -### Evaluation -Using the prefix 'e-' in hungarian style notation, the value of the attribute will be assumed to be an expression -evaluated on the parent scope. -``` - -``` - -### Binding -Perhaps the least likely scenario is to set up two-way property binding. The title will be set to the value of the -parent scope, and a two-way binding will be set up with the component's property. -``` - -``` - -### Special attributes -In order to configure components with some reasonable defaults, there are a few special directive attributes. - -- config - An angular expression that returns an object used for Ext.create(). All other config attributes will be added to this config object with ```configs = _.extend(configs, attributeValue)``` - - example - ```config="{title:myTitle}"``` -- bindTo - A variable name on the parent scope to bind the ext component to. -- renderTo - Establishes what DOM element to render the component to. - - ```false``` or ```''```: Doesn't set the config at all (You'll probably need to use 'add') - - ```true```: sets 'renderTo' to the directives HTML element. - - ```[string]```: passes the id through to the Ext renderTo config. -- add - For adding components to parent containers. - - ```false``` or ```''```: No behavior. You probably want to use 'renderTo' in this case. - - ```true```: Adds this component to the immediate parent directive/component (assuming the parent is a container) - - ```[string]```: Add this component to the component found at given property on the parent scope (Least likely) - -TODO ----- - -- [ ] Hungarian style attribute evaluation - - [ ] Interpolated attributes - - [ ] Evaluated attributes - - [ ] Two-way binding -- [ ] Virtual element directives that add to parent 'items' +Much of this repo and library is a work in progress. There aren't any special Angular-EXT adapters or bindings yet, but this will be the place for them. Angular's Digest and Ext Events ------------------------------- @@ -96,73 +22,8 @@ a $digest as well as $emit those events on the parent scope. DataStores ---------- -??? -Things like find() and datastore results can be set up as functions. -```javascript -$scope.results = function() { return store.find(...) } -``` - -Stores should be observables that can be bound to the digest cycle just like components. They'll get constructed -on the scope and manipulated with $watch if they need to be bound to things like filter changes. +WIP. Conceptually, data stores can be adapted to provide angular promises. Motivation ---------- -The goal is to be able to do the following - -```javascript -$scope.title = 'Border Layout'; -``` -```html - - - - - -``` - -### TODO -- [ ] - Determine how child elements get rolled into 'items' for their parent. Shouldn't this be the default behavior? -- [ ] - Component directive that includes child html as 'template' for creating containers with markup - -Instead of this - -```javascript -var panelVar = Ext.create('Ext.panel.Panel', { - width: 500, - height: 300, - title: 'Border Layout', - layout: 'border', - items: [{ - title: 'South Region is resizable', - region: 'south', // position for region - xtype: 'panel', - height: 100, - split: true, // enable resizing - margins: '0 5 5 5' - },{ - // xtype: 'panel' implied by default - title: 'West Region is collapsible', - region:'west', - xtype: 'panel', - margins: '5 0 0 5', - width: 200, - collapsible: true, // make collapsible - id: 'west-region-container', - layout: 'fit' - },{ - title: 'Center Region', - region: 'center', // center region is required, no width/height specified - xtype: 'panel', - layout: 'fit', - margins: '5 5 0 0' - }], - renderTo: Ext.getBody() -}); -``` -And, to allow the App SDK to effortlessly include angular apps and components. -``` -Ext.create('Rally.anuglar.app', {module: 'myModule'}) -``` - -### TODO -- [ ] Create an ext container class to bootstrap angular. +To allow the App SDK to effortlessly include angular apps and components. From afd4e94565acdf3d605cca4073f5a9aa7c3723cb Mon Sep 17 00:00:00 2001 From: pcw216 Date: Tue, 15 Jul 2014 09:51:45 -0400 Subject: [PATCH 5/5] Wsapi project scope down loader --- dist/rally.js | 137 +++++++++++++++++++- src/rally/api/wsapi/index.coffee | 4 + src/rally/api/wsapi/projects.coffee | 62 +++++++++ src/rally/util/http/index.coffee | 1 + src/rally/util/http/services/promise.coffee | 19 +++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/rally/api/wsapi/index.coffee create mode 100644 src/rally/api/wsapi/projects.coffee create mode 100644 src/rally/util/http/services/promise.coffee diff --git a/dist/rally.js b/dist/rally.js index 618fbf0..9021c14 100644 --- a/dist/rally.js +++ b/dist/rally.js @@ -104,6 +104,110 @@ angular.module('rally.api.services.wsapi', ['rally.api.services.slm', 'rally.uti return this; }); +angular.module('rally.api.wsapi', ['rally.api.services.wsapi', 'rally.api.wsapi.projects']); + +var RallyApiWsapiProjects, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +angular.module('rally.api.wsapi.projects', ['rally.util.http.services.promise', 'rally.api.services.wsapi', 'rally.util.async']).run(function($wsapi, rallyApiWsapiProjects) { + return $wsapi.projects = rallyApiWsapiProjects; +}).service('rallyApiWsapiProjects', RallyApiWsapiProjects = (function() { + function RallyApiWsapiProjects($q, $wsapi, httpPromise) { + this.$q = $q; + this.$wsapi = $wsapi; + this.httpPromise = httpPromise; + this._scopeUp = __bind(this._scopeUp, this); + this._scopeDown = __bind(this._scopeDown, this); + this.scope = __bind(this.scope, this); + this.children = __bind(this.children, this); + this.concurrencyLimit = 4; + } + + /* + @param {object} projectScope - an object with 'oid' and 'workspaceOid' properties + Workspaces should have identical oid and workspaceOid + */ + + + RallyApiWsapiProjects.prototype.children = function(projectScope, onlyOpen) { + var type, url; + if (onlyOpen == null) { + onlyOpen = true; + } + type = projectScope.oid === projectScope.workspaceOid ? 'workspace' : 'project'; + url = "/" + type + "/" + projectScope.oid + "/Children"; + if (onlyOpen) { + url += '?query=(State != "Closed")'; + } + return this.httpPromise.asArray(this.$wsapi({ + url: url, + method: 'JSONP', + params: { + 'jsonp': 'JSON_CALLBACK' + } + }).then(function(response) { + return response.data.QueryResult.Results; + })); + }; + + /* + @param {object} projectScope - an object with 'oid' and 'workspaceOid' properties + Workspaces should have identical oid and workspaceOid + @param {string} direction - 'up' or 'down' [default] to load a project tree + */ + + + RallyApiWsapiProjects.prototype.scope = function(projectScope, direction) { + if (direction == null) { + direction = 'down'; + } + switch (direction) { + case 'down': + return this._scopeDown(projectScope); + case 'up': + return this._scopeUp(projectScope); + } + }; + + RallyApiWsapiProjects.prototype._scopeDown = function(projectScope, concurrency) { + var deferred, queue, + _this = this; + if (concurrency == null) { + concurrency = this.concurrencyLimit; + } + deferred = this.$q.defer(); + queue = async.queue(function(_arg, callback) { + var projectScope; + projectScope = _arg.projectScope; + deferred.notify(projectScope); + projectScope.children = _this.children(projectScope); + projectScope.name = projectScope.Name; + return projectScope.children.$promise.then(function(children) { + _.each(children, function(child) { + child.oid = child.ObjectID; + child.workspaceOid = projectScope.workspaceOid; + return queue.push({ + projectScope: child + }); + }); + return callback(); + }); + }, concurrency); + queue.drain = function() { + return deferred.resolve(); + }; + queue.push({ + projectScope: projectScope + }); + return deferred.promise; + }; + + RallyApiWsapiProjects.prototype._scopeUp = function(projectScope) {}; + + return RallyApiWsapiProjects; + +})()); + var __slice = [].slice; angular.module('rally.app.iframe.decorators.rootScope', []).config(function($provide) { @@ -339,7 +443,38 @@ angular.module('rally.util.http.factories.httpWrapper', ['rally.util.lodash']).f }; }); -angular.module('rally.util.http', ['rally.util.http.factories.httpWrapper']); +angular.module('rally.util.http', ['rally.util.http.factories.httpWrapper', 'rally.util.http.services.promise']); + +var HttpPromise; + +angular.module('rally.util.http.services.promise', ['rally.util.lodash']).service('httpPromise', HttpPromise = (function() { + function HttpPromise() {} + + HttpPromise.prototype.asArray = function(promise) { + var data; + data = []; + data.$promise = promise; + promise.then(function(results) { + return _.each(results, function(r) { + return data.push(r); + }); + }); + return data; + }; + + HttpPromise.prototype.asObject = function(promise) { + var data; + data = {}; + data.$promise = promise; + promise.then(function(result) { + return _.merge(data, result); + }); + return data; + }; + + return HttpPromise; + +})()); angular.module('rally.util.lodash', ['rally.util.lodash.sortedInsert']); diff --git a/src/rally/api/wsapi/index.coffee b/src/rally/api/wsapi/index.coffee new file mode 100644 index 0000000..fec90f1 --- /dev/null +++ b/src/rally/api/wsapi/index.coffee @@ -0,0 +1,4 @@ +angular.module('rally.api.wsapi', [ + 'rally.api.services.wsapi' + 'rally.api.wsapi.projects' +]) diff --git a/src/rally/api/wsapi/projects.coffee b/src/rally/api/wsapi/projects.coffee new file mode 100644 index 0000000..f1ecbe9 --- /dev/null +++ b/src/rally/api/wsapi/projects.coffee @@ -0,0 +1,62 @@ +angular.module('rally.api.wsapi.projects', [ + 'rally.util.http.services.promise' + 'rally.api.services.wsapi' + 'rally.util.async' +]) + +.run ($wsapi, rallyApiWsapiProjects)-> + $wsapi.projects = rallyApiWsapiProjects + +.service 'rallyApiWsapiProjects', + class RallyApiWsapiProjects + constructor: (@$q, @$wsapi, @httpPromise)-> + @concurrencyLimit = 4 + + ### + @param {object} projectScope - an object with 'oid' and 'workspaceOid' properties + Workspaces should have identical oid and workspaceOid + ### + children: (projectScope, onlyOpen=true)=> + type = if projectScope.oid is projectScope.workspaceOid then 'workspace' else 'project' + url = "/#{type}/#{projectScope.oid}/Children" + if onlyOpen then url += '?query=(State != "Closed")' + + return @httpPromise.asArray @$wsapi({ + url: url + method: 'JSONP', + params: { + 'jsonp': 'JSON_CALLBACK' + } + }).then (response)-> + return response.data.QueryResult.Results + + ### + @param {object} projectScope - an object with 'oid' and 'workspaceOid' properties + Workspaces should have identical oid and workspaceOid + @param {string} direction - 'up' or 'down' [default] to load a project tree + ### + scope: (projectScope, direction='down')=> + switch direction + when 'down' then return @_scopeDown(projectScope) + when 'up' then return @_scopeUp(projectScope) + + _scopeDown: (projectScope, concurrency=@concurrencyLimit)=> + deferred = @$q.defer() + queue = async.queue(({projectScope}, callback)=> + deferred.notify(projectScope) + projectScope.children = @children(projectScope) + projectScope.name = projectScope.Name + # Only finish when children have finished loading + projectScope.children.$promise.then (children)-> + _.each children, (child)-> + child.oid = child.ObjectID + child.workspaceOid = projectScope.workspaceOid + queue.push({projectScope: child}) + callback() + , concurrency) + queue.drain = ()-> + deferred.resolve() + queue.push({projectScope}) + return deferred.promise + + _scopeUp: (projectScope)=> diff --git a/src/rally/util/http/index.coffee b/src/rally/util/http/index.coffee index 3321061..211acd4 100644 --- a/src/rally/util/http/index.coffee +++ b/src/rally/util/http/index.coffee @@ -1,3 +1,4 @@ angular.module('rally.util.http', [ 'rally.util.http.factories.httpWrapper' + 'rally.util.http.services.promise' ]) diff --git a/src/rally/util/http/services/promise.coffee b/src/rally/util/http/services/promise.coffee new file mode 100644 index 0000000..767f5a4 --- /dev/null +++ b/src/rally/util/http/services/promise.coffee @@ -0,0 +1,19 @@ +angular.module('rally.util.http.services.promise', [ + 'rally.util.lodash' +]) +.service 'httpPromise', + + class HttpPromise + asArray: (promise)-> + data = [] + data.$promise = promise + promise.then (results)-> + _.each results, (r)-> data.push(r) + return data + + asObject: (promise)-> + data = {} + data.$promise = promise + promise.then (result)-> + _.merge data, result + return data