diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index c3f37e1528..f5ca57ab69 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -33,6 +33,8 @@ import {PortalController} from 'tower/controllers/Portal'; import dataServices from 'tower/services/_data-services'; import dashboardGraphs from 'tower/directives/_dashboard-graphs'; +import routeExtensions from 'tower/shared/route-extensions/main'; + import {JobDetailController} from 'tower/controllers/JobDetail'; import {JobStdoutController} from 'tower/controllers/JobStdout'; import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from 'tower/controllers/JobTemplates'; @@ -71,6 +73,7 @@ var tower = angular.module('Tower', [ 'RestServices', dataServices.name, dashboardGraphs.name, + routeExtensions.name, 'AuthService', 'Utilities', 'LicenseHelper', @@ -184,6 +187,7 @@ var tower = angular.module('Tower', [ $routeProvider. when('/jobs', { + name: 'jobs', templateUrl: urlPrefix + 'partials/jobs.html', controller: JobsListController, resolve: { @@ -204,6 +208,7 @@ var tower = angular.module('Tower', [ }). when('/jobs/:id', { + name: 'jobDetail', templateUrl: urlPrefix + 'partials/job_detail.html', controller: JobDetailController, resolve: { diff --git a/awx/ui/static/js/docs.js b/awx/ui/static/js/docs.js index 8f51c66bf2..6011068888 100644 --- a/awx/ui/static/js/docs.js +++ b/awx/ui/static/js/docs.js @@ -1 +1,2 @@ import 'tower/shared/multi-select-list/main.js'; +import 'tower/shared/route-extensions/main.js'; diff --git a/awx/ui/static/js/shared/route-extensions/link-to.directive.js b/awx/ui/static/js/shared/route-extensions/link-to.directive.js new file mode 100644 index 0000000000..b79ffaef99 --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/link-to.directive.js @@ -0,0 +1,74 @@ +import {lookupRouteUrl} from './lookup-route-url'; + +/** + * + * @ngdoc directive + * @name routeExtensions.directive:linkTo + * @desription + * The `linkTo` directive looks up a route's URL and generates a link to that route. When a user + * clicks the link, this directive calls the `transitionTo` factory to send them to the given + * URL. For accessibility and fallback purposes, it also sets the `href` attribute of the link + * to the path. + * + * Note that in this example the model object uses a key that matches up with the route parameteer + * name in the route url (in this case `:id`). + * + * **N.B.** The below example currently won't run. It's included to show an example of using + * the `linkTo` directive within code. In order for this to run, we will need to run + * the code in an iframe (using something like `dgeni` instead of `grunt-ngdocs`). + * + * @example + * + + + angular.module('simpleRouteExample', ['ngRoute', 'routeExtensions']) + .config(['$routeProvider', function($route) { + $route.when('/posts/:id', { + name: 'post', + template: '

{{post.title}}

{{post.body}}

', + controller: 'post' + }); + }]).controller('post', function($scope) { + }); +
+ +
+ + {{featuredPost.title}} + +
+
+
+ * + */ +export default + [ '$route', + 'transitionTo', + function($routeProvider, transitionTo) { + return { + restrict: 'E', + transclude: true, + template: '', + scope: { + routeName: '@route', + model: '&' + }, + link: function(scope, element) { + + var model = scope.$eval(scope.model); + scope.url = lookupRouteUrl(scope.routeName, $routeProvider.routes, model); + + element.find('[data-transition-to]').on('click', function(e) { + e.stopPropagation(); + e.preventDefault(); + transitionTo(scope.routeName, model); + }); + + } + }; + } + ]; diff --git a/awx/ui/static/js/shared/route-extensions/lookup-route-url.js b/awx/ui/static/js/shared/route-extensions/lookup-route-url.js new file mode 100644 index 0000000000..54e505a2ae --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/lookup-route-url.js @@ -0,0 +1,43 @@ +export function lookupRouteUrl(name, routes, models) { + var route = _.find(routes, {name: name}); + + if (angular.isUndefined(route)) { + throw "Unknown route " + name; + } + + var routeUrl = route.originalPath; + + if (!angular.isUndefined(models) && angular.isObject(models)) { + var match = routeUrl.match(route.regexp); + var keyMatchers = match.slice(1); + + routeUrl = + keyMatchers.reduce(function(url, keyMatcher) { + var value; + var key = keyMatcher.replace(/^:/, ''); + + var model = models[key]; + + if (angular.isArray(model)) { + value = _.compact(_.pluck(model, key)); + + if (_.isEmpty(value)) { + value = _.pluck(model, 'id'); + } + + value = value.join(','); + } else if (angular.isObject(model)) { + value = model[key]; + + if (_.isEmpty(value)) { + value = model.id; + } + } + + return url.replace(keyMatcher, value); + }, routeUrl); + + } + + return routeUrl; +} diff --git a/awx/ui/static/js/shared/route-extensions/main.js b/awx/ui/static/js/shared/route-extensions/main.js new file mode 100644 index 0000000000..f041598bd9 --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/main.js @@ -0,0 +1,28 @@ +import linkTo from './link-to.directive'; +import transitionTo from './transition-to.factory'; +import modelListener from './model-listener.config'; + +/** + * @ngdoc overview + * @name routeExtensions + * @description + * + * # routeExtensions + * + * Adds a couple useful features to ngRoute: + * - Adds a `name` property to route objects; used to identify the route in transitions & links + * - Adds the ability to pass model data when clicking a link that goes to a route + * - Adds a directive that generates a route's URL from the route name & given models + * - Adds the ability to specify models in route resolvers + * + * ## Usage + * + * If you need to generate a link to a route, then use the {@link routeExtensions.directive:linkTo `linkTo directive`}. If you need to transition to a route in JavaScript code, then use the {@link routeExtensions.factory:transitionTo `transitionTo service`}. + * +*/ +export default + angular.module('routeExtensions', + ['ngRoute']) + .factory('transitionTo', transitionTo) + .run(modelListener) + .directive('linkTo', linkTo); diff --git a/awx/ui/static/js/shared/route-extensions/model-listener.config.js b/awx/ui/static/js/shared/route-extensions/model-listener.config.js new file mode 100644 index 0000000000..7f7112f08a --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/model-listener.config.js @@ -0,0 +1,17 @@ +export default + [ '$rootScope', + '$routeParams', + function($rootScope, $routeParams) { + $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { + if (angular.isUndefined(newRoute.model)) { + var keys = Object.keys(newRoute.params); + var models = keys.reduce(function(model, key) { + model[key] = newRoute.locals[key]; + return model; + }, {}); + + $routeParams.model = models; + } + }); + } + ]; diff --git a/awx/ui/static/js/shared/route-extensions/transition-to.factory.js b/awx/ui/static/js/shared/route-extensions/transition-to.factory.js new file mode 100644 index 0000000000..6acee4326f --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/transition-to.factory.js @@ -0,0 +1,131 @@ +import {lookupRouteUrl} from './lookup-route-url'; + +/** + * @ngdoc service + * @name routeExtensions.service:transitionTo + * @description + * The `transitionTo` service generates a URL given a route name and model parameters, then + * updates the browser's URL via `$location.path`. Use this in situations where you cannot + * use the `linkTo` directive, for example to redirect the user after saving an object. + * + * @param {string} routeName The name of the route whose URL you want to redirect to (corresponds + * name property of route) + * @param {object} model The model you want to use to generate the URL and be passed to the new + * route. This object follows a strict key/value naming convention where + * the keys match the parameters listed in the route's URL. For example, + * a URL of `/posts/:id` would require a model object like: `{ id: post }`, + * where `post` is the object you want to pass to the new route. + * + * **N.B.** The below example currently won't run. It's included to show an example of using + * the `transitionTo` function within code. In order for this to run, we will need to run + * the code in an iframe (using something like `dgeni` instead of `grunt-ngdocs`). + * + * @example + * + + + angular.module('transitionToExample', + ['ngRoute', + 'routeExtensions' + ]) + .config(function($routeProvider, $locationProvider) { + $routeProvider + .when('/post/:id', + { name: 'post', + template: '

{{post.title}}

{{post.body}}

' + }); + + $locationProvider.html5Mode(true); + $locationProvider.hashPrefix('!'); + }) + .controller('post', ['$scope', function($scope) { + + }]) + .controller('postForm', ['$scope', 'transitionTo', function($scope, transitionTo) { + $scope.post = { + id: 1, + title: 'A post', + body: 'Some text' + }; + + $scope.savePost = function() { + transitionTo('post', { id: $scope.post }); + } + }]); +
+ +
+ Edit Post + + + + +
+
+ +
+ + */ + +function safeApply(fn, $rootScope) { + var currentPhase = $rootScope.$$phase; + + if (currentPhase === '$apply' || currentPhase === '$digest') { + fn(); + } else { + $rootScope.$apply(fn); + } +} + +export default + [ '$location', + '$rootScope', + '$route', + '$q', + function($location, $rootScope, $route, $q) { + return function(routeName, model) { + var deferred = $q.defer(); + var url = lookupRouteUrl(routeName, $route.routes, model); + + var offRouteChangeStart = + $rootScope.$on('$routeChangeStart', function(e, newRoute) { + if (newRoute.$$route.name === routeName) { + deferred.resolve(newRoute, model); + newRoute.params.model = model; + } + + offRouteChangeStart(); + }); + + var offRouteChangeSuccess = + $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { + if (newRoute.$$route.name === routeName) { + deferred.resolve(newRoute); + } + + offRouteChangeSuccess(); + }); + + var offRouteChangeError = + $rootScope.$on('$routeChangeError', function(e, newRoute, previousRoute, rejection) { + if (newRoute.$$route.name === routeName) { + deferred.reject(newRoute, previousRoute, rejection); + } + + offRouteChangeError(); + }); + + safeApply(function() { + $location.path(url); + }, $rootScope); + + return deferred; + }; + } + ];