diff --git a/src/app/search/controller/search_controller.js b/src/app/search/controller/search_controller.js new file mode 100644 index 00000000..e75ad1e8 --- /dev/null +++ b/src/app/search/controller/search_controller.js @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * This controller provides initialization logic for the generic search view. + */ +angular.module('sb.search').controller('SearchController', + function ($log, $q, $scope, Criteria, $stateParams) { + 'use strict'; + + /** + * Default criteria, potentially populated by the q param. + * + * @type {Array} + */ + $scope.defaultCriteria = []; + + /** + * List of resource types which this view will be searching on. + * + * @type {string[]} + */ + $scope.resourceTypes = ['Story', 'Project', 'User', 'Task']; + + /** + * If a 'q' exists in the state params, go ahead and add it. + */ + if ($stateParams.hasOwnProperty('q') && !!$stateParams.q) { + $scope.defaultCriteria.push( + Criteria.create('Text', $stateParams.q) + ); + } + } +); diff --git a/src/app/search/controller/search_criteria_controller.js b/src/app/search/controller/search_criteria_controller.js index f8be8f20..a3329c28 100644 --- a/src/app/search/controller/search_criteria_controller.js +++ b/src/app/search/controller/search_criteria_controller.js @@ -16,18 +16,19 @@ /** * The sole purpose of this controller is to allow a user to search for valid - * search/filter criteria, and expose chosen criteria to the scope. These - * criteria may either be resources, resource identifiers (type/id pairs), - * or plain strings. + * search/filter criteria for various resources, and expose chosen criteria + * to the scope. These criteria may be static or asynchronously loaded, and + * may be property filters (title = foo) or resource filters (story_id = 22). */ angular.module('sb.search').controller('SearchCriteriaController', - function ($log, $q, $scope, Criteria, Browse, $stateParams) { + function ($log, $q, $scope, Criteria) { 'use strict'; /** - * Valid sets of resources that can be searched on. + * Valid sets of resources that can be searched on. The default + * assumes no resources may be searched. */ - var resourceTypes = ['Story', 'Project', 'User', 'Task']; + var resourceTypes = []; /** * Managed list of active criteria tags. @@ -37,32 +38,34 @@ angular.module('sb.search').controller('SearchCriteriaController', $scope.criteria = []; /** - * When a criteria is added, make sure we remove duplicates - the - * control doesn't handle that for us. + * Initialize this controller with different resource types and + * default search criteria. + * + * @param types + * @param defaultCriteria + */ + $scope.init = function (types, defaultCriteria) { + resourceTypes = types || resourceTypes; + $scope.criteria = defaultCriteria || []; + $scope.searchForCriteria = + Criteria.buildCriteriaSearch(resourceTypes); + }; + + /** + * When a criteria is added, make sure we remove all previous criteria + * that have the same type. */ $scope.addCriteria = function (item) { - var idx = $scope.criteria.indexOf(item); - - for (var i = 0; i < $scope.criteria.length; i++) { + for (var i = $scope.criteria.length - 1; i >= 0; i--) { var cItem = $scope.criteria[i]; // Don't remove exact duplicates. - if (idx === i) { + if (cItem === item) { continue; } - // We can only search for one text type at a time. - if (item.type === 'text' && - cItem.type === 'text') { + if (item.type === cItem.type) { $scope.criteria.splice(i, 1); - break; - } - - // Remove any duplicate value types. - if (item.type === cItem.type && - item.value === cItem.value) { - $scope.criteria.splice(i, 1); - break; } } }; @@ -99,30 +102,10 @@ angular.module('sb.search').controller('SearchCriteriaController', /** * Search for available search criteria. */ - $scope.searchForCriteria = function (searchString) { + $scope.searchForCriteria = function () { var deferred = $q.defer(); - - searchString = searchString || ''; - - Browse.all(searchString).then(function (results) { - - // Add text. - results.unshift(Criteria.create('text', searchString)); - - deferred.resolve(results); - }); - - // Return the search promise. + deferred.resolve([]); return deferred.promise; }; - - /** - * If a 'q' exists in the state params, go ahead and add it. - */ - if ($stateParams.hasOwnProperty('q') && !!$stateParams.q) { - $scope.criteria.push( - Criteria.create('text', $stateParams.q) - ); - } } ); diff --git a/src/app/search/module.js b/src/app/search/module.js index aa824c54..33c0c737 100644 --- a/src/app/search/module.js +++ b/src/app/search/module.js @@ -27,6 +27,7 @@ angular.module('sb.search', $stateProvider .state('search', { url: '/search?q', - templateUrl: 'app/search/template/index.html' + templateUrl: 'app/search/template/index.html', + controller: 'SearchController' }); }); diff --git a/src/app/search/template/criteria_tag_item.html b/src/app/search/template/criteria_tag_item.html new file mode 100644 index 00000000..474a6a0c --- /dev/null +++ b/src/app/search/template/criteria_tag_item.html @@ -0,0 +1,60 @@ + + + +
+ {{tag.value}} + + × + +
+
+ {{tag.title}} + + × + +
+
+ {{tag.title}} + + × + +
+
+ Story Status: {{tag.title}} + + × + +
+
+ {{tag.title}} + + × + +
+
+ {{tag.type}}: {{tag.value}} + + × + +
+
\ No newline at end of file diff --git a/src/app/search/template/index.html b/src/app/search/template/index.html index fa163907..296ee735 100644 --- a/src/app/search/template/index.html +++ b/src/app/search/template/index.html @@ -13,7 +13,8 @@ ~ License for the specific language governing permissions and limitations ~ under the License. --> -
+

Search

@@ -28,7 +29,7 @@ tag-complete-tags="criteria" tag-complete-label-field="title" tag-complete-option-template-url="'app/search/template/typeahead_criteria_item.html'" - tag-complete-tag-template-url="'/inline/criteria_tag_item.html'" + tag-complete-tag-template-url="'app/search/template/criteria_tag_item.html'" tag-complete-loading="loadingCriteria = isLoading" tag-complete-on-select="addCriteria(tag)">
@@ -235,44 +236,4 @@
-
- - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/search/template/typeahead_criteria_item.html b/src/app/search/template/typeahead_criteria_item.html index a9a0d6b4..34a4d761 100644 --- a/src/app/search/template/typeahead_criteria_item.html +++ b/src/app/search/template/typeahead_criteria_item.html @@ -1,16 +1,20 @@ - +  {{match.model.title}} - +  {{match.model.value}}: {{match.model.title}} - + +  Story Status: + {{match.model.title}} + +  {{match.model.title}} - +  {{match.model.title}} diff --git a/src/app/services/criteria/criteria.js b/src/app/services/criteria/criteria.js new file mode 100644 index 00000000..db4cf2a3 --- /dev/null +++ b/src/app/services/criteria/criteria.js @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * A service which centralizes management of search criteria: Creation, + * validation, filtering, criteria-to-parameter mapping, and more. + */ +angular.module('sb.services').service('Criteria', + function ($q, $log, $injector) { + 'use strict'; + + return { + + /** + * This method takes a set of criteria, and filters out the + * ones not valid for the passed resource. + * + * @param resourceName The name of the resource to filter for. + * @param criteria The list of criteria. + * @return {Array} A map of URL parameters. + */ + filterCriteria: function (resourceName, criteria) { + + var resource = $injector.get(resourceName); + + // Sanity check: If we don't have this resource, wat? + if (!resource || !resource.hasOwnProperty('criteriaFilter')) { + $log.warn('Attempting to filter criteria for unknown ' + + 'resource "' + resourceName + '"'); + return []; + } + + return resource.criteriaFilter(criteria); + }, + + /** + * This method takes a set of criteria, and maps them against the + * query parameters available for the provided resource. It will + * skip any items not valid for this resource, and return an + * array of criteria that are valid + * + * @param resourceName + * @param criteria + * @return A map of URL parameters. + */ + mapCriteria: function (resourceName, criteria) { + var resource = $injector.get(resourceName); + + // Sanity check: If we don't have this resource, wat? + if (!resource || !resource.hasOwnProperty('criteriaMap')) { + $log.warn('Attempting to map criteria for unknown ' + + 'resource "' + resourceName + '"'); + return {}; + } + + return resource.criteriaMap(criteria); + }, + + /** + * Create a new build criteria object. + * + * @param type The type of the criteria tag. + * @param value Value of the tag. Unique DB ID, or text string. + * @param title The title of the criteria tag. + * @returns {Criteria} + */ + create: function (type, value, title) { + title = title || value; + return { + 'type': type, + 'value': value, + 'title': title + }; + }, + + /** + * Rather than actually performing a search, this method returns a + * customized lambda that will perform our browse search for us. + * + * @param types An array of resource types to browse. + */ + buildCriteriaSearch: function (types) { + var resolvers = []; + types.forEach(function (type) { + // Retrieve an instance of the declared resource. + var resource = $injector.get(type); + + if (!resource.hasOwnProperty('criteriaResolvers')) { + $log.warn('Resource type "' + type + + '" does not implement criteriaResolvers.'); + return; + } + + resource.criteriaResolvers().forEach(function (resolver) { + if (resolvers.indexOf(resolver) === -1) { + resolvers.push(resolver); + } + }); + }); + + /** + * Construct the search lambda that issues the search + * and assembles the results. + */ + return function (searchString) { + var deferred = $q.defer(); + + // Clear the criteria + var promises = []; + + resolvers.forEach(function (resolver) { + promises.push(resolver(searchString)); + }); + + // Wrap everything into a collective promise + $q.all(promises).then(function (results) { + var criteria = []; + + results.forEach(function (result) { + result.forEach(function (item) { + criteria.push(item); + }); + }); + deferred.resolve(criteria); + }); + + // Return the search promise. + return deferred.promise; + }; + }, + + /** + * This method takes a set of criteria, and filters out the + * ones not valid for the passed resource. + * + * @param parameterMap A map of criteria types and parameters + * in the search query they correspond to. + * @return {Function} A criteria filter for the passed parameters. + */ + buildCriteriaFilter: function (parameterMap) { + return function (criteria) { + var filteredCriteria = []; + + criteria.forEach(function (item) { + if (parameterMap.hasOwnProperty(item.type)) { + filteredCriteria.push(item); + } + }); + return filteredCriteria; + }; + }, + + /** + * This method takes a set of criteria, and maps them against the + * query parameters available for the provided resource. It will + * skip any items not valid for this resource, and return an + * array of criteria that are valid + * + * @param parameterMap A map of criteria types and parameters + * in the search query they correspond to. + * @return {Function} A criteria mapper for the passed parameters. + */ + buildCriteriaMap: function (parameterMap) { + return function (criteria) { + var params = {}; + + criteria.forEach(function (item) { + if (parameterMap.hasOwnProperty(item.type)) { + params[parameterMap[item.type]] = item.value; + } + }); + return params; + }; + } + }; + } +); diff --git a/src/app/services/criteria/story_status.js b/src/app/services/criteria/story_status.js new file mode 100644 index 00000000..c7625c8e --- /dev/null +++ b/src/app/services/criteria/story_status.js @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + + +/** + * This criteria resolver may be injected by individual resources that accept a + * Story Status search parameters. + */ +angular.module('sb.services').factory('StoryStatus', + function (Criteria, $q) { + 'use strict'; + + /** + * A list of valid story status items. + * + * @type {*[]} + */ + var validStatusCriteria = [ + Criteria.create('StoryStatus', 'active', 'Active'), + Criteria.create('StoryStatus', 'merged', 'Merged'), + Criteria.create('StoryStatus', 'invalid', 'Invalid') + ]; + + /** + * Return a criteria resolver for story status. + */ + return { + criteriaResolver: function (searchString) { + var deferred = $q.defer(); + searchString = searchString || ''; // Sanity check + searchString = searchString.toLowerCase(); // Lowercase search + + var criteria = []; + validStatusCriteria.forEach(function (criteriaItem) { + var title = criteriaItem.title.toLowerCase(); + + // If we match the title, OR someone is explicitly typing in + // 'status' + if (title.indexOf(searchString) > -1 || + 'status'.indexOf(searchString) === 0) { + criteria.push(criteriaItem); + } + }); + deferred.resolve(criteria); + + return deferred.promise; + } + }; + }); \ No newline at end of file diff --git a/src/app/services/criteria/text.js b/src/app/services/criteria/text.js new file mode 100644 index 00000000..72ab7f22 --- /dev/null +++ b/src/app/services/criteria/text.js @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + + +/** + * This criteria resolver may be injected by individual resources that accept a + * plain text search parameter. + */ +angular.module('sb.services').factory('Text', + function (Criteria, $q) { + 'use strict'; + + /** + * Return a text search parameter constructed from the passed search + * string. + */ + return { + criteriaResolver: function (searchString) { + var deferred = $q.defer(); + + deferred.resolve([Criteria.create('Text', searchString)]); + + return deferred.promise; + } + }; + }); \ No newline at end of file diff --git a/src/app/services/provider/storyboard_api_signature.js b/src/app/services/provider/storyboard_api_signature.js deleted file mode 100644 index 859dbd95..00000000 --- a/src/app/services/provider/storyboard_api_signature.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain - * a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -/** - * In lieu of extension, here we're injecting our common API signature that - * can be reused by all of our services. - * - * @author Michael Krotscheck - */ -angular.module('sb.services') - .factory('storyboardApiSignature', function (pageSize) { - 'use strict'; - - return { - 'create': { - method: 'POST' - }, - 'read': { - method: 'GET', - cache: false - }, - 'update': { - method: 'PUT' - }, - 'delete': { - method: 'DELETE' - }, - 'query': { - method: 'GET', - isArray: true, - responseType: 'json', - params: { - limit: pageSize - } - } - }; - } -); \ No newline at end of file diff --git a/src/app/services/resource/browse.js b/src/app/services/resource/browse.js deleted file mode 100644 index 564fefed..00000000 --- a/src/app/services/resource/browse.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain - * a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -/** - * A browse service, which wraps common resources and their typeahead - * resolution into a single service that returns a common result format. - * It is paired with the Criteria service to provide a consistent data - * format to identify resources independent of their actual schema. - */ -angular.module('sb.services').factory('Browse', - function ($q, $log, Project, Story, User, Criteria) { - 'use strict'; - - return { - - /** - * Browse projects by search string. - * - * @param searchString A string to search by. - * @return A promise that will resolve with the search results. - */ - project: function (searchString) { - // Search for projects... - var deferred = $q.defer(); - - Project.query({name: searchString}, - function (result) { - // Transform the results to criteria tags. - var projResults = []; - result.forEach(function (item) { - projResults.push( - Criteria.create('project', item.id, item.name) - ); - }); - deferred.resolve(projResults); - }, function () { - deferred.resolve([]); - } - ); - - return deferred.promise; - }, - - /** - * Browse users by search string. - * - * @param searchString A string to search by. - * @return A promise that will resolve with the search results. - */ - user: function (searchString) { - - // Search for users... - var deferred = $q.defer(); - User.query({full_name: searchString}, - function (result) { - // Transform the results to criteria tags. - var userResults = []; - result.forEach(function (item) { - userResults.push( - Criteria.create('user', item.id, item.full_name) - ); - }); - deferred.resolve(userResults); - }, function () { - deferred.resolve([]); - } - ); - - return deferred.promise; - }, - - /** - * Browse stories by search string. - * - * @param searchString A string to search by. - * @return A promise that will resolve with the search results. - */ - story: function (searchString) { - - // Search for stories... - var deferred = $q.defer(); - Story.query({title: searchString}, - function (result) { - // Transform the results to criteria tags. - var storyResults = []; - result.forEach(function (item) { - storyResults.push( - Criteria.create('story', item.id, item.title) - ); - }); - deferred.resolve(storyResults); - }, function () { - deferred.resolve([]); - } - ); - - return deferred.promise; - }, - - - /** - * Browse all resources by a provided search string. - * - * @param searchString - * @return A promise that will resolve with the search results. - */ - all: function (searchString) { - var deferred = $q.defer(); - - // Clear the criteria - var criteria = []; - - // Wrap everything into a collective promise - $q.all({ - projects: this.project(searchString), - stories: this.story(searchString), - users: this.user(searchString) - }).then(function (results) { - // Add the returned projects to the results list. - results.projects.forEach(function (item) { - criteria.push(item); - }); - // Add the returned stories to the results list. - results.stories.forEach(function (item) { - criteria.push(item); - }); - // Add the returned stories to the results list. - results.users.forEach(function (item) { - criteria.push(item); - }); - deferred.resolve(criteria); - }); - - // Return the search promise. - return deferred.promise; - } - }; - }); diff --git a/src/app/services/resource/comment.js b/src/app/services/resource/comment.js index 885773a0..b2053d5c 100644 --- a/src/app/services/resource/comment.js +++ b/src/app/services/resource/comment.js @@ -21,13 +21,15 @@ * @see storyboardApiSignature */ angular.module('sb.services').factory('Comment', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/stories/:story_id/comments/:id', + return ResourceFactory.build( + '/stories/:story_id/comments/:id', + '/stories/0/search', { id: '@id', story_id: '@story_id' - }, - storyboardApiSignature); + } + ); }); \ No newline at end of file diff --git a/src/app/services/resource/criteria.js b/src/app/services/resource/criteria.js deleted file mode 100644 index 834168d9..00000000 --- a/src/app/services/resource/criteria.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. - * - * Licensed under the Apache License, Version 2.0 (the 'License'); you may - * not use this file except in compliance with the License. You may obtain - * a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -/** - * A service which centralizes management of search criteria: Creation, - * validation, filtering, criteria-to-parameter mapping, and more. - */ -angular.module('sb.services').service('Criteria', - function ($log) { - 'use strict'; - - var resourceParams = { - Project: { - text: 'name' - }, - Story: { - project: 'project_id', - text: 'title', - user: 'assignee_id' - }, - Task: { - story: 'story_id', - user: 'assignee_id' - }, - User: { - text: 'full_name' - } - }; - - return { - - /** - * Is this resource name valid? - * - * @param resourceName - * @returns {boolean} - */ - isValidResource: function (resourceName) { - return resourceParams.hasOwnProperty(resourceName); - }, - - - /** - * This method takes a set of criteria, and filters out the - * ones not valid for the passed resource. - * - * @param resourceName The name of the resource to filter for. - * @param criteria The list of criteria. - * @return {Array} A map of URL parameters. - */ - filterCriteria: function (resourceName, criteria) { - - // Sanity check: If we don't have this resource, wat? - if (!this.isValidResource(resourceName)) { - $log.warn('Attempting to filter criteria for unknown ' + - 'resource "' + resourceName + '"'); - return []; - } - - var filteredCriteria = []; - var mapping = resourceParams[resourceName]; - - criteria.forEach(function (item) { - if (mapping.hasOwnProperty(item.type)) { - filteredCriteria.push(item); - } - }); - return filteredCriteria; - }, - - /** - * This method takes a set of criteria, and maps them against the - * query parameters available for the provided resource. It will - * skip any items not valid for this resource, and return an - * array of criteria that are valid - * - * @param resourceName - * @param criteria - * @return A map of URL parameters. - */ - mapCriteria: function (resourceName, criteria) { - // Sanity check: If we don't have this resource, wat? - if (!this.isValidResource(resourceName)) { - $log.warn('Attempting to filter criteria for unknown ' + - 'resource "' + resourceName + '"'); - return []; - } - - var params = {}; - var mapping = resourceParams[resourceName]; - - criteria.forEach(function (item) { - if (mapping.hasOwnProperty(item.type)) { - params[mapping[item.type]] = item.value; - } - }); - return params; - }, - - /** - * Create a new build criteria object. - * - * @param type The type of the criteria tag. - * @param value Value of the tag. Unique DB ID, or text string. - * @param title The title of the criteria tag. - * @returns {Criteria} - */ - create: function (type, value, title) { - title = title || value; - return { - 'type': type, - 'value': value, - 'title': title - }; - } - }; - } -); diff --git a/src/app/services/resource/project.js b/src/app/services/resource/project.js index cc128de9..4667f7f8 100644 --- a/src/app/services/resource/project.js +++ b/src/app/services/resource/project.js @@ -18,14 +18,24 @@ * The angular resource abstraction that allows us to access projects and their * details. * - * @see storyboardApiSignature + * @see ResourceFactory * @author Michael Krotscheck */ angular.module('sb.services').factory('Project', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/projects/:id', - {id: '@id'}, - storyboardApiSignature); + var resource = ResourceFactory.build( + '/projects/:id', + '/projects/search', + {id: '@id'} + ); + + ResourceFactory.applyBrowse( + 'Project', + resource, + {Text: 'name'} + ); + + return resource; }); \ No newline at end of file diff --git a/src/app/services/resource/project_group.js b/src/app/services/resource/project_group.js index 2675b7bd..ef8806f3 100644 --- a/src/app/services/resource/project_group.js +++ b/src/app/services/resource/project_group.js @@ -17,14 +17,16 @@ /** * The angular resource abstraction that allows us to access projects groups. * - * @see storyboardApiSignature + * @see ResourceFactory * @author Michael Krotscheck */ angular.module('sb.services').factory('ProjectGroup', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/project_groups/:id', - {id: '@id'}, - storyboardApiSignature); + return ResourceFactory.build( + '/project_groups/:id', + '/project_groups/search', + {id: '@id'} + ); }); \ No newline at end of file diff --git a/src/app/services/resource/story.js b/src/app/services/resource/story.js index 411bd2e9..589c0fb8 100644 --- a/src/app/services/resource/story.js +++ b/src/app/services/resource/story.js @@ -20,10 +20,25 @@ * @see storyboardApiSignature */ angular.module('sb.services').factory('Story', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/stories/:id', - {id: '@id'}, - storyboardApiSignature); + var resource = ResourceFactory.build( + '/stories/:id', + '/stories/search', + {id: '@id'} + ); + + ResourceFactory.applyBrowse( + 'Story', + resource, + { + Text: 'title', + StoryStatus: 'status', + Project: 'project_id', + User: 'assignee_id' + } + ); + + return resource; }); \ No newline at end of file diff --git a/src/app/services/resource/task.js b/src/app/services/resource/task.js index 91a11b60..1d65fb7c 100644 --- a/src/app/services/resource/task.js +++ b/src/app/services/resource/task.js @@ -21,10 +21,23 @@ * @author Michael Krotscheck */ angular.module('sb.services').factory('Task', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/tasks/:id', - {id: '@id'}, - storyboardApiSignature); + var resource = ResourceFactory.build( + '/tasks/:id', + '/tasks/search', + {id: '@id'} + ); + + ResourceFactory.applyBrowse( + 'Task', + resource, + { + Story: 'story_id', + User: 'assignee_id' + } + ); + + return resource; }); \ No newline at end of file diff --git a/src/app/services/resource/team.js b/src/app/services/resource/team.js index 39e341a6..57d4500e 100644 --- a/src/app/services/resource/team.js +++ b/src/app/services/resource/team.js @@ -22,10 +22,14 @@ * @author Michael Krotscheck */ angular.module('sb.services').factory('Team', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/teams/:id', - {id: '@id'}, - storyboardApiSignature); + return ResourceFactory.build( + '/teams/:id', + '/teams/search', // Not implemented. + { + id: '@id' + } + ); }); \ No newline at end of file diff --git a/src/app/services/resource/timeline_event.js b/src/app/services/resource/timeline_event.js index 9ac40d09..6f4370a2 100644 --- a/src/app/services/resource/timeline_event.js +++ b/src/app/services/resource/timeline_event.js @@ -21,13 +21,15 @@ * @see storyboardApiSignature */ angular.module('sb.services').factory('TimelineEvent', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/stories/:story_id/events/:id', + return ResourceFactory.build( + '/stories/:story_id/events/:id', + '/stories/:story_id/events/search', // Not implemented. { id: '@id', story_id: '@story_id' - }, - storyboardApiSignature); + } + ); }); \ No newline at end of file diff --git a/src/app/services/resource/user.js b/src/app/services/resource/user.js index 598f7046..39fe10ee 100644 --- a/src/app/services/resource/user.js +++ b/src/app/services/resource/user.js @@ -18,14 +18,24 @@ * The angular resource abstraction that allows us to search, access, and * modify users. * - * @see storyboardApiSignature + * @see ResourceFactory * @author Michael Krotscheck */ angular.module('sb.services').factory('User', - function ($resource, storyboardApiBase, storyboardApiSignature) { + function (ResourceFactory) { 'use strict'; - return $resource(storyboardApiBase + '/users/:id', - {id: '@id'}, - storyboardApiSignature); + var resource = ResourceFactory.build( + '/users/:id', + '/users/search', + {id: '@id'} + ); + + ResourceFactory.applyBrowse( + 'User', + resource, + {Text: 'full_name'} + ); + + return resource; }); \ No newline at end of file diff --git a/src/app/services/service/resource_factory.js b/src/app/services/service/resource_factory.js new file mode 100644 index 00000000..2ebb3bce --- /dev/null +++ b/src/app/services/service/resource_factory.js @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * Factory methods that simply construction of storyboard API resources. + * + * @author Michael Krotscheck + */ +angular.module('sb.services') + .service('ResourceFactory', + function ($q, $log, $injector, Criteria, $resource, storyboardApiBase) { + 'use strict'; + + /** + * This method is used in our API signature to return a recent value + * for the user's pageSize preference. + * + * @returns {*} + */ + function getLimit() { + return $injector.get('pageSize'); + } + + /** + * Construct a full API signature for a specific resource. Includes + * CRUD, Browse, and Search. If the resource doesn't support it, + * don't use it :). + * + * @param searchUrl + * @returns An API signature that may be used with a $resource. + */ + function buildSignature(searchUrl) { + return { + 'create': { + method: 'POST' + }, + 'read': { + method: 'GET', + cache: false + }, + 'update': { + method: 'PUT' + }, + 'delete': { + method: 'DELETE' + }, + 'browse': { + method: 'GET', + isArray: true, + responseType: 'json', + params: { + limit: getLimit + } + }, + 'search': { + method: 'GET', + url: searchUrl, + isArray: true, + responseType: 'json', + params: { + limit: getLimit + } + } + }; + } + + + return { + + /** + * Build a resource URI. + * + * @param restUri + * @param searchUri + * @param resourceParameters + * @returns {*} + */ + build: function (restUri, searchUri, resourceParameters) { + + if (!restUri) { + $log.error('Cannot use resource factory ' + + 'without a base REST uri.'); + return null; + } + + var signature = buildSignature(storyboardApiBase + searchUri); + return $resource(storyboardApiBase + restUri, + resourceParameters, signature); + }, + + /** + * This method takes an already configured resource, and applies + * the static methods necessary to support the criteria browse API. + * Browse parameters should be formatted as an object containing + * 'injector name': 'param'. For example, {'Project': 'project_id'}. + * + * @param resourceName The explicit resource name of this resource + * within the injection scope. + * @param resource The configured resource. + * @param browseParameters The browse parameters to apply. + */ + applyBrowse: function (resourceName, resource, browseParameters) { + + // List of criteria resolvers which we're building. + var criteriaResolvers = []; + var browseParameter = null; // Default is '' + + for (var type in browseParameters) { + + // Store the browse parameter for later. + if (type === 'Text') { + browseParameter = browseParameters[type]; + } + + // If the requested type exists and has a criteriaResolver + // method, add it to the list of resolvable browse criteria. + var typeResource = $injector.get(type); + if (!!typeResource && + typeResource.hasOwnProperty('criteriaResolver')) { + criteriaResolvers.push(typeResource.criteriaResolver); + } + } + + /** + * Return a list of promise-returning methods that, given a + * browse string, will provide a list of search criteria. + * + * @returns {*[]} + */ + resource.criteriaResolvers = function () { + return criteriaResolvers; + }; + + + // If we found a browse parameter, add the ability to use + // this resource as a source of criteria. + if (!!browseParameter) { + /** + * Add the criteria resolver method. + */ + resource.criteriaResolver = function (searchString) { + + var deferred = $q.defer(); + + // build the query parameters. + var queryParams = {}; + queryParams[browseParameter] = searchString; + + resource.query(queryParams, + function (result) { + // Transform the results to criteria tags. + var criteriaResults = []; + result.forEach(function (item) { + criteriaResults.push( + Criteria.create(resourceName, + item.id, + item[browseParameter]) + ); + }); + deferred.resolve(criteriaResults); + }, function () { + deferred.resolve([]); + } + ); + + return deferred.promise; + }; + } + + + /** + * The criteria filter. + */ + resource.criteriaFilter = Criteria + .buildCriteriaFilter(browseParameters); + + /** + * The criteria map. + */ + resource.criteriaMap = Criteria + .buildCriteriaMap(browseParameters); + + } + }; + }); diff --git a/src/app/storyboard/controller/header_controller.js b/src/app/storyboard/controller/header_controller.js index 0b395e71..cb50368e 100644 --- a/src/app/storyboard/controller/header_controller.js +++ b/src/app/storyboard/controller/header_controller.js @@ -20,8 +20,8 @@ */ angular.module('storyboard').controller('HeaderController', function ($q, $scope, $rootScope, $state, NewStoryService, Session, - SessionState, CurrentUser, Browse, Criteria, Notification, - Priority) { + SessionState, CurrentUser, Criteria, Notification, + Priority, Project, Story) { 'use strict'; function resolveCurrentUser() { @@ -72,13 +72,13 @@ angular.module('storyboard').controller('HeaderController', $scope.search = function (criteria) { switch (criteria.type) { - case 'text': + case 'Text': $state.go('search', {q: criteria.value}); break; - case 'project': + case 'Project': $state.go('project.detail', {id: criteria.value}); break; - case 'story': + case 'Story': $state.go('story.detail', {storyId: criteria.value}); break; } @@ -96,12 +96,12 @@ angular.module('storyboard').controller('HeaderController', searchString = searchString || ''; $q.all({ - projects: Browse.project(searchString), - stories: Browse.story(searchString) + projects: Project.criteriaResolver(searchString), + stories: Story.criteriaResolver(searchString) }).then(function (results) { var criteria = [ - Criteria.create('text', searchString) + Criteria.create('Text', searchString) ]; // Add the returned projects to the results list. diff --git a/test/unit/services/provider/storyboard_api_signature_test.js b/test/unit/services/provider/storyboard_api_signature_test.js deleted file mode 100644 index 32617029..00000000 --- a/test/unit/services/provider/storyboard_api_signature_test.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain - * a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -/** - * This test suite verifies that our default API request signature is - * sane. - */ -describe('storyboardApiSignature', function () { - 'use strict'; - - beforeEach(module('sb.services')); - - it('should exist', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature).toBeTruthy(); - }); - }); - - it('should declare CRUD methods', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature.create).toBeTruthy(); - expect(storyboardApiSignature.read).toBeTruthy(); - expect(storyboardApiSignature.update).toBeTruthy(); - expect(storyboardApiSignature.delete).toBeTruthy(); - }); - }); - - it('should declare a query method', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature.query).toBeTruthy(); - }); - }); - - it('should use POST to create', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature.create).toBeTruthy(); - expect(storyboardApiSignature.create.method).toEqual('POST'); - }); - }); - it('should use GET to read', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature.read).toBeTruthy(); - expect(storyboardApiSignature.read.method).toEqual('GET'); - }); - }); - it('should use PUT to update', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature.update).toBeTruthy(); - expect(storyboardApiSignature.update.method).toEqual('PUT'); - }); - }); - it('should use DELETE to delete', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature.delete).toBeTruthy(); - expect(storyboardApiSignature.delete.method).toEqual('DELETE'); - }); - }); - it('should use GET to query', function () { - inject(function (storyboardApiSignature) { - expect(storyboardApiSignature.query).toBeTruthy(); - expect(storyboardApiSignature.query.method).toEqual('GET'); - }); - }); - - it('should properly construct a resource', function () { - inject(function (storyboardApiSignature, $resource) { - - var Resource = $resource('/path/:id', - {id: '@id'}, - storyboardApiSignature); - expect(Resource.query).toBeTruthy(); - expect(Resource.read).toBeTruthy(); - - var resourceInstance = new Resource(); - expect(resourceInstance.$create).toBeTruthy(); - expect(resourceInstance.$update).toBeTruthy(); - expect(resourceInstance.$delete).toBeTruthy(); - }); - }); -});