From 062836f2dd436befb4f3e5bf1234ca37f7267288 Mon Sep 17 00:00:00 2001 From: Michael Krotscheck Date: Mon, 3 Feb 2014 18:09:58 +0100 Subject: [PATCH] MVP Storyboard Client This patch constitutes the work discussed in Brussels for the MVP webclient. Changes made, as follows: 1- Updated header to contain Overview, Projects, Stories, and a "New Story" button, including mobile treatment. 2- Updated navigation header to use selection styling for mobile and regular. 3- Application index contains new-story button. 4- Main application container now has a minheight. 5- Removed old controllers (code cleanup) before reimplementation. 6- Wired project views (require API support). 7- Wired story views (require API support). 8- Wired new story modal (require API support). 9- New task form (requires API support). 10- Wired up new functional tests and fixed a few issues in existing unit tests. 11- Switched LESS compiler from recess to less, since recess can no longer compile the most recent version of bootstrap. Change-Id: Iddd5b29dd899d92f05ad7a9a63814d8599e167d4 --- Gruntfile.js | 25 +- bower.json | 40 +- package.json | 10 +- .../project_group_new_controller.js | 54 -- src/app/project_groups/module.js | 55 -- .../controllers/project_detail_controller.js | 45 +- .../controllers/project_list_controller.js | 36 +- .../project_story_list_controller.js} | 58 +- src/app/projects/module.js | 29 +- .../provider/storyboard_api_signature.js | 5 +- src/app/services/resource/story.js | 29 + .../controllers/story_detail_controller.js} | 92 ++- .../controllers/story_list_controller.js} | 48 +- .../controllers/story_modal_controller.js | 47 ++ src/app/stories/module.js | 71 ++ src/app/stories/services/new_story_service.js | 47 ++ .../controllers/header_controller.js | 27 + .../storyboard/controllers/home_controller.js | 27 + .../{storyboard.js => storyboard/module.js} | 9 +- src/app/teams/module.js | 40 -- src/app/templates/footer.html | 8 +- src/app/templates/header.html | 119 ++-- src/app/templates/index.html | 24 +- src/app/templates/project/delete.html | 34 + src/app/templates/project/detail.html | 45 +- src/app/templates/project/edit.html | 112 ++-- src/app/templates/project/list.html | 104 +-- src/app/templates/project/new.html | 3 +- .../detail.html => project/overview.html} | 25 +- src/app/templates/project/stories.html | 97 +++ src/app/templates/project_groups/edit.html | 85 --- src/app/templates/project_groups/list.html | 87 --- src/app/templates/project_groups/new.html | 74 --- src/app/templates/story/delete.html | 34 + src/app/templates/story/detail.html | 39 ++ src/app/templates/story/edit.html | 64 ++ src/app/templates/story/list.html | 106 +++ src/app/templates/story/new.html | 72 +++ src/app/templates/story/overview.html | 120 ++++ src/app/templates/teams/list.html | 87 --- src/app/util/directive/active_path.js | 15 +- src/index.html | 28 +- src/styles/body.less | 9 +- src/styles/bootstrap_addons.less | 118 +++- src/styles/bootstrap_variable_overrides.less | 604 ------------------ src/styles/main.less | 5 +- src/styles/variables.less | 31 + test/functional/auth/auth_route.js | 29 - test/functional/projects/routes.js | 24 + test/functional/stories/routes.js | 24 + .../provider/storyboard_api_signature_test.js | 12 +- 51 files changed, 1533 insertions(+), 1499 deletions(-) delete mode 100644 src/app/project_groups/controllers/project_group_new_controller.js delete mode 100644 src/app/project_groups/module.js rename src/app/{project_groups/controllers/project_group_list_controller.js => projects/controllers/project_story_list_controller.js} (50%) create mode 100644 src/app/services/resource/story.js rename src/app/{project_groups/controllers/project_group_detail_controller.js => stories/controllers/story_detail_controller.js} (60%) rename src/app/{teams/controllers/teams_list_controller.js => stories/controllers/story_list_controller.js} (57%) create mode 100644 src/app/stories/controllers/story_modal_controller.js create mode 100644 src/app/stories/module.js create mode 100644 src/app/stories/services/new_story_service.js create mode 100644 src/app/storyboard/controllers/header_controller.js create mode 100644 src/app/storyboard/controllers/home_controller.js rename src/app/{storyboard.js => storyboard/module.js} (86%) delete mode 100644 src/app/teams/module.js create mode 100644 src/app/templates/project/delete.html rename src/app/templates/{project_groups/detail.html => project/overview.html} (60%) create mode 100644 src/app/templates/project/stories.html delete mode 100644 src/app/templates/project_groups/edit.html delete mode 100644 src/app/templates/project_groups/list.html delete mode 100644 src/app/templates/project_groups/new.html create mode 100644 src/app/templates/story/delete.html create mode 100644 src/app/templates/story/detail.html create mode 100644 src/app/templates/story/edit.html create mode 100644 src/app/templates/story/list.html create mode 100644 src/app/templates/story/new.html create mode 100644 src/app/templates/story/overview.html delete mode 100644 src/app/templates/teams/list.html delete mode 100644 src/styles/bootstrap_variable_overrides.less create mode 100644 src/styles/variables.less delete mode 100644 test/functional/auth/auth_route.js create mode 100644 test/functional/projects/routes.js create mode 100644 test/functional/stories/routes.js diff --git a/Gruntfile.js b/Gruntfile.js index c0ee7b0c..2b5c8773 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -101,7 +101,6 @@ module.exports = function (grunt) { concat: { dist: { src: [ - dir.source + '/app/storyboard.js', dir.source + '/app/**/module.js', dir.source + '/app/**/*.js' ], @@ -119,19 +118,20 @@ module.exports = function (grunt) { * Note: We're using LessCSS here because SASS requires ruby-compass, * and cannot be easily installed with npm. */ - recess: { + less: { options: { - includePath: [ - dir.bower + '/bootstrap/less/', + paths: [ + dir.bower + '/bootstrap/less', dir.bower + '/font-awesome/less/' ], - compile: true + cleancss: true, + strictMath: true, + strictUnits: true }, theme: { - src: [ - dir.source + '/styles/main.less' - ], - dest: dir.output + '/styles/main.css' + files: { + 'dist/styles/main.css': dir.source + '/styles/main.less' + } } }, @@ -346,17 +346,16 @@ module.exports = function (grunt) { watch: { concat: { files: [ - dir.source + '/app/storyboard.js', dir.source + '/app/**/module.js', dir.source + '/app/**/*.js' ], tasks: ['concat'] }, - recess: { + less: { files: [ dir.source + '/styles/**/*.less' ], - tasks: ['recess:theme'] + tasks: ['less:theme'] }, copy: { files: [ @@ -505,7 +504,7 @@ module.exports = function (grunt) { 'jshint', 'useminPrepare', 'concat', - 'recess', + 'less', 'imagemin', 'html2js', 'copy', diff --git a/bower.json b/bower.json index dd619b28..313ae6d2 100644 --- a/bower.json +++ b/bower.json @@ -1,22 +1,22 @@ { - "name": "storyboard-webclient", - "version": "0.0.1", - "dependencies": { - "jquery": "2.0.3", - "font-awesome": "4.0", - "angular": "1.2.9", - "angular-resource": "1.2.9", - "angular-cookies": "1.2.9", - "angular-sanitize": "1.2.9", - "bootstrap": "3.0.0", - "angular-ui-router": "0.2.7", - "angular-translate": "1.1.1" - }, - "devDependencies": { - "angular-mocks": "1.2.9", - "angular-scenario": "1.2.9" - }, - "resolutions": { - "angular": "1.2.9" - } + "name": "storyboard-webclient", + "version": "0.0.1", + "dependencies": { + "jquery": "2.0.3", + "font-awesome": "4.0", + "angular": "1.2.13", + "angular-resource": "1.2.13", + "angular-cookies": "1.2.13", + "angular-sanitize": "1.2.13", + "bootstrap": "3.1.0", + "angular-ui-router": "0.2.8-bowratic-tedium", + "angular-bootstrap": "0.10.0" + }, + "devDependencies": { + "angular-mocks": "1.2.13", + "angular-scenario": "1.2.13" + }, + "resolutions": { + "angular": "1.2.13" + } } diff --git a/package.json b/package.json index 66b4b9da..8fe889d7 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,13 @@ "grunt-usemin": "2.0.2", "grunt-contrib-htmlmin": "0.1.3", "grunt-contrib-cssmin": "0.7.0", + "grunt-contrib-less": "0.9.0", "grunt-karma": "0.6.2", "grunt-contrib-connect": "0.5.0", "grunt-contrib-watch": "0.5.3", "grunt-contrib-jshint": "0.7.2", "grunt-contrib-uglify": "0.2.7", "grunt-contrib-imagemin": "0.4.0", - "grunt-recess": "0.5.0", "grunt": "0.4.2", "grunt-cli": "0.1.11", "matchdep": "0.1.2", @@ -49,10 +49,10 @@ "grunt-shell": "0.6.1", "karma-coverage": "0.1.4", "grunt-env": "0.4.1", - "protractor": "0.15.0", - "grunt-protractor-runner": "0.2.0", + "protractor": "0.19.0", + "grunt-protractor-runner": "0.2.3", "selenium-standalone": "2.39.0-2.7.0", - "karma-html-reporter": "~0.1.1", - "grunt-connect-proxy": "~0.1.7" + "karma-html-reporter": "0.1.1", + "grunt-connect-proxy": "0.1.8" } } diff --git a/src/app/project_groups/controllers/project_group_new_controller.js b/src/app/project_groups/controllers/project_group_new_controller.js deleted file mode 100644 index d3025ce4..00000000 --- a/src/app/project_groups/controllers/project_group_new_controller.js +++ /dev/null @@ -1,54 +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. - */ - -/** - * View controller for the new project group form. Includes an intermediary - * 'saving' flag as well as room for an error response (though until we get - * a real API that'll be a bit tricky to test). - */ -angular.module('sb.project_groups').controller('ProjectGroupNewController', - function ($scope, $state, ProjectGroup) { - 'use strict'; - - // View parameters. - $scope.newProjectGroup = new ProjectGroup(); - $scope.isCreating = false; - $scope.error = {}; - - /** - * Submits the newly created project. If an error response is received, - * assigns it to the view and unsets various flags. The template - * should know how to handle it. - */ - $scope.createProjectGroup = function () { - - // Clear everything and set the progress flag... - $scope.isCreating = true; - $scope.error = {}; - - $scope.newProjectGroup.$create( - function () { - // Success! - $state.go('project_groups.list'); - }, - function (error) { - // Error received. Ho hum. - $scope.isCreating = false; - $scope.error = error; - } - ); - }; - }); diff --git a/src/app/project_groups/module.js b/src/app/project_groups/module.js deleted file mode 100644 index a74c0f37..00000000 --- a/src/app/project_groups/module.js +++ /dev/null @@ -1,55 +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. - */ - -/** - * The Storyboard project_group submodule handles most activity surrounding the - * creation and management of project_groups. - */ -angular.module('sb.project_groups', ['ui.router', 'sb.services', 'sb.util']) - .config(function ($stateProvider, $urlRouterProvider) { - 'use strict'; - - // URL Defaults. - $urlRouterProvider.when('/project_groups', '/project_groups/list'); - - // Set our page routes. - $stateProvider - .state('project_groups', { - abstract: true, - url: '/project_groups', - template: '
' - }) - .state('project_groups.list', { - url: '/list', - templateUrl: 'app/templates/project_groups/list.html', - controller: 'ProjectGroupListController' - }) - .state('project_groups.edit', { - url: '/{id:[0-9]+}/edit', - templateUrl: 'app/templates/project_groups/edit.html', - controller: 'ProjectGroupDetailController' - }) - .state('project_groups.detail', { - url: '/{id:[0-9]+}', - templateUrl: 'app/templates/project_groups/detail.html', - controller: 'ProjectGroupDetailController' - }) - .state('project_groups.new', { - url: '/new', - templateUrl: 'app/templates/project_groups/new.html', - controller: 'ProjectGroupNewController' - }); - }); diff --git a/src/app/projects/controllers/project_detail_controller.js b/src/app/projects/controllers/project_detail_controller.js index ac5ae718..ab56a22a 100644 --- a/src/app/projects/controllers/project_detail_controller.js +++ b/src/app/projects/controllers/project_detail_controller.js @@ -1,11 +1,11 @@ /* - * Copyright (c) 2013 Hewlett-Packard Development Company, L.P. + * 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 + * 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 @@ -28,7 +28,7 @@ * seconds. 3 is preferable. */ angular.module('sb.projects').controller('ProjectDetailController', - function ($scope, $state, $stateParams, Project) { + function ($scope, $state, $stateParams, Project, Story) { 'use strict'; // Parse the ID @@ -43,6 +43,15 @@ angular.module('sb.projects').controller('ProjectDetailController', */ $scope.project = {}; + /** + * The count of stories for this project. + * + * TODO(krotscheck): Once we have proper paging requests working, + * this should become a count-only request, so we can delegate project + * story searches to the ProjectStoryListController. + */ + $scope.projectStoryCount = 0; + /** * UI flag for when we're initially loading the view. * @@ -98,6 +107,15 @@ angular.module('sb.projects').controller('ProjectDetailController', }, handleServiceError ); + // Load the count of stories while we're at it... + Story.query({project: id}, + function (result, headers) { + // Only extract the total header... + $scope.projectStoryCount = + headers('X-List-Total') || result.length; + }, + handleServiceError + ); } /** @@ -118,25 +136,4 @@ angular.module('sb.projects').controller('ProjectDetailController', handleServiceError ); }; - - - /** - * Scope method, invoke this when you'd like to delete this project. - */ - $scope.remove = function () { - // Set our progress flags and clear previous error conditions. - $scope.isUpdating = true; - $scope.error = {}; - - // Try to delete. - $scope.project.$delete( - function () { - // The deletion was successful, so head back to the list - // view. - $scope.isUpdating = false; - $state.go('project.list'); - }, - handleServiceError - ); - }; }); diff --git a/src/app/projects/controllers/project_list_controller.js b/src/app/projects/controllers/project_list_controller.js index 6135dc93..9bc627e5 100644 --- a/src/app/projects/controllers/project_list_controller.js +++ b/src/app/projects/controllers/project_list_controller.js @@ -1,11 +1,11 @@ /* - * Copyright (c) 2013 Hewlett-Packard Development Company, L.P. + * 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 + * 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 @@ -24,7 +24,15 @@ angular.module('sb.projects').controller('ProjectListController', 'use strict'; // Variables and methods available to the template... - $scope.projects = []; + + function resetScope() { + $scope.projectCount = 0; + $scope.projectOffset = 0; + $scope.projectLimit = 10; + $scope.projects = []; + $scope.error = {}; + } + $scope.searchQuery = ''; $scope.isSearching = false; @@ -33,18 +41,25 @@ angular.module('sb.projects').controller('ProjectListController', */ $scope.search = function () { // Clear the scope and set the progress flag. - $scope.error = {}; + resetScope(); $scope.isSearching = true; - $scope.projects = []; // Execute the project query. - Project.search( - // Enable this once the API's there, mocks don't support - // searches yet - {/* q: $scope.searchQuery || '' */}, - function (result) { + Project.query( + // Enable this once the API accepts search queries. + { /*q: $scope.searchQuery || ''*/}, + function (result, headers) { + + // Extract metadata from returned headers. + var projectCount = headers('X-List-Total') || result.length; + var projectOffset = headers('X-List-Offset') || 0; + var projectLimit = headers('X-List-Limit') || result.length; + // Successful search results, apply the results to the // scope and unset our progress flag. + $scope.projectCount = projectCount; + $scope.projectOffset = projectOffset; + $scope.projectLimit = projectLimit; $scope.projects = result; $scope.isSearching = false; }, @@ -58,5 +73,6 @@ angular.module('sb.projects').controller('ProjectListController', }; // Initialize the view with a default search. + resetScope(); $scope.search(); }); diff --git a/src/app/project_groups/controllers/project_group_list_controller.js b/src/app/projects/controllers/project_story_list_controller.js similarity index 50% rename from src/app/project_groups/controllers/project_group_list_controller.js rename to src/app/projects/controllers/project_story_list_controller.js index 7fb775fa..f525fad1 100644 --- a/src/app/project_groups/controllers/project_group_list_controller.js +++ b/src/app/projects/controllers/project_story_list_controller.js @@ -15,16 +15,33 @@ */ /** - * The projectGroup list controller handles discovery for all projectGroups, - * including search. Note that it is assumed that we implemented a search - * (inclusive), rather than a browse (exclusive) approach. + * This controller manages stories within the scope of a particular project. */ -angular.module('sb.project_groups').controller('ProjectGroupListController', - function ($scope, ProjectGroup) { +angular.module('sb.projects').controller('ProjectStoryListController', + function ($scope, $state, $stateParams, Story, NewStoryService) { 'use strict'; + // Parse the ID. Since we're in a nested state, we don't really need + // to sanity check here, but in case of a future refactor we'll + // go ahead and do so anyway. + var id = $stateParams.hasOwnProperty('id') ? + parseInt($stateParams.id, 10) : + null; + + if (id === null) { + $state.go('index'); + return; + } + // Variables and methods available to the template... - $scope.projectGroups = []; + function resetScope() { + $scope.storyCount = 0; + $scope.storyOffset = 0; + $scope.storyLimit = 10; + $scope.stories = []; + $scope.error = {}; + } + $scope.searchQuery = ''; $scope.isSearching = false; @@ -33,19 +50,25 @@ angular.module('sb.project_groups').controller('ProjectGroupListController', */ $scope.search = function () { // Clear the scope and set the progress flag. - $scope.error = {}; + resetScope(); $scope.isSearching = true; - $scope.projectGroups = []; - // Execute the projectGroup query. - ProjectGroup.search( - // Enable this once the API's there, mocks don't support - // searches yet - {/* q: $scope.searchQuery || '' */}, - function (result) { + // Execute the story query. + Story.query( + {project: id}, + function (result, headers) { + + // Extract metadata from returned headers. + var storyCount = headers('X-List-Total') || result.length; + var storyOffset = headers('X-List-Offset') || 0; + var storyLimit = headers('X-List-Limit') || result.length; + // Successful search results, apply the results to the // scope and unset our progress flag. - $scope.projectGroups = result; + $scope.storyCount = storyCount; + $scope.storyOffset = storyOffset; + $scope.storyLimit = storyLimit; + $scope.stories = result; $scope.isSearching = false; }, function (error) { @@ -57,6 +80,11 @@ angular.module('sb.project_groups').controller('ProjectGroupListController', ); }; + $scope.newStory = function () { + NewStoryService.showNewStoryModal(id); + }; + // Initialize the view with a default search. + resetScope(); $scope.search(); }); diff --git a/src/app/projects/module.js b/src/app/projects/module.js index b535dd90..a6bcee58 100644 --- a/src/app/projects/module.js +++ b/src/app/projects/module.js @@ -22,8 +22,12 @@ angular.module('sb.projects', ['ui.router', 'sb.services', 'sb.util']) .config(function ($stateProvider, $urlRouterProvider) { 'use strict'; - // URL Defaults. + // Routing Defaults. $urlRouterProvider.when('/project', '/project/list'); + $urlRouterProvider.when('/project/{id:[0-9]+}', + function ($match) { + return '/project/' + $match.id + '/overview'; + }); // Set our page routes. $stateProvider @@ -37,16 +41,29 @@ angular.module('sb.projects', ['ui.router', 'sb.services', 'sb.util']) templateUrl: 'app/templates/project/list.html', controller: 'ProjectListController' }) - .state('project.edit', { - url: '/{id:[0-9]+}/edit', - templateUrl: 'app/templates/project/edit.html', - controller: 'ProjectDetailController' - }) .state('project.detail', { + abstract: true, url: '/{id:[0-9]+}', templateUrl: 'app/templates/project/detail.html', controller: 'ProjectDetailController' }) + .state('project.detail.overview', { + url: '/overview', + templateUrl: 'app/templates/project/overview.html' + }) + .state('project.detail.edit', { + url: '/edit', + templateUrl: 'app/templates/project/edit.html' + }) + .state('project.detail.delete', { + url: '/delete', + templateUrl: 'app/templates/project/delete.html' + }) + .state('project.detail.stories', { + url: '/stories', + templateUrl: 'app/templates/project/stories.html', + controller: 'ProjectStoryListController' + }) .state('project.new', { url: '/new', templateUrl: 'app/templates/project/new.html', diff --git a/src/app/services/provider/storyboard_api_signature.js b/src/app/services/provider/storyboard_api_signature.js index 4a9a5ff4..22fef42a 100644 --- a/src/app/services/provider/storyboard_api_signature.js +++ b/src/app/services/provider/storyboard_api_signature.js @@ -38,9 +38,10 @@ angular.module('sb.services') 'delete': { method: 'DELETE' }, - 'search': { + 'query': { method: 'GET', - isArray: true + isArray: true, + responseType: 'json' } }; } diff --git a/src/app/services/resource/story.js b/src/app/services/resource/story.js new file mode 100644 index 00000000..411bd2e9 --- /dev/null +++ b/src/app/services/resource/story.js @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * The angular resource abstraction that allows us to access stories. + * + * @see storyboardApiSignature + */ +angular.module('sb.services').factory('Story', + function ($resource, storyboardApiBase, storyboardApiSignature) { + 'use strict'; + + return $resource(storyboardApiBase + '/stories/:id', + {id: '@id'}, + storyboardApiSignature); + }); \ No newline at end of file diff --git a/src/app/project_groups/controllers/project_group_detail_controller.js b/src/app/stories/controllers/story_detail_controller.js similarity index 60% rename from src/app/project_groups/controllers/project_group_detail_controller.js rename to src/app/stories/controllers/story_detail_controller.js index d0da42c0..89369857 100644 --- a/src/app/project_groups/controllers/project_group_detail_controller.js +++ b/src/app/stories/controllers/story_detail_controller.js @@ -13,36 +13,25 @@ * License for the specific language governing permissions and limitations * under the License. */ + /** - * Project group detail & manipulation controller. Usable for any view that - * wants to view, edit, or delete a project group, though views don't have to - * use all the functions therein. Includes flags for busy time, error responses - * and more. - * - * This controller assumes that the $stateParams object is both injectable and - * contains an ":id" property that indicates which project should be loaded. At - * the moment it will only set a 'isLoading' flag to indicate that data is - * loading. If loading the data is anticipated to take longer than 3 seconds, - * this will need to be updated to display a sane progress. - * - * Do not allow loading of this (or any) controller to take longer than 10 - * seconds. 3 is preferable. + * Story detail & manipulation controller. */ -angular.module('sb.project_groups').controller('ProjectGroupDetailController', - function ($scope, $state, $stateParams, ProjectGroup) { +angular.module('sb.story').controller('StoryDetailController', + function ($scope, $state, $stateParams, Story, Task) { 'use strict'; // Parse the ID - var id = $stateParams.hasOwnProperty('id') ? - parseInt($stateParams.id, 10) : + var id = $stateParams.hasOwnProperty('storyId') ? + parseInt($stateParams.storyId, 10) : null; /** - * The project group we're manipulating right now. - * - * @type ProjectGroup + * The story we're manipulating right now. */ - $scope.projectGroup = {}; + $scope.story = {}; + $scope.tasks = []; + $scope.newTask = new Task(); /** * UI flag for when we're initially loading the view. @@ -76,6 +65,20 @@ angular.module('sb.project_groups').controller('ProjectGroupDetailController', $scope.isUpdating = false; } + /** + * Loads the tasks for this story + */ + function loadTasks() { + $scope.tasks = []; + + Task.query( + {story: id}, + function (result) { + $scope.tasks = result; + }, + handleServiceError + ); + } // Sanity check, do we actually have an ID? (zero is falsy) if (!id && id !== 0) { @@ -89,18 +92,32 @@ angular.module('sb.project_groups').controller('ProjectGroupDetailController', $scope.isLoading = false; } else { // We've got an ID, so let's load it... - ProjectGroup.read( + Story.read( {'id': id}, function (result) { // We've got a result, assign it to the view and unset our // loading flag. - $scope.projectGroup = result; + $scope.story = result; $scope.isLoading = false; }, handleServiceError ); + + loadTasks(); } + /** + * Adds a task. + */ + $scope.addTask = function () { + $scope.newTask.story_id = id; + $scope.newTask.$save(function () { + loadTasks(); + $scope.newTask = new Task(); + }); + }; + + /** * Scope method, invoke this when you want to update the project. */ @@ -110,34 +127,11 @@ angular.module('sb.project_groups').controller('ProjectGroupDetailController', $scope.error = {}; // Invoke the save method and wait for results. - $scope.projectGroup.$update( - function () { + $scope.story.$update( + function (result) { // Unset our loading flag and navigate to the detail view. $scope.isUpdating = false; - $state.go('project_groups.detail', { - id: $scope.projectGroup.id - }); - }, - handleServiceError - ); - }; - - - /** - * Scope method, invoke this when you'd like to delete this project. - */ - $scope.remove = function () { - // Set our progress flags and clear previous error conditions. - $scope.isUpdating = true; - $scope.error = {}; - - // Try to delete. - $scope.projectGroup.$delete( - function () { - // The deletion was successful, so head back to the list - // view. - $scope.isUpdating = false; - $state.go('project_groups.list'); + $state.go('story.detail.overview', {storyId: result.id}); }, handleServiceError ); diff --git a/src/app/teams/controllers/teams_list_controller.js b/src/app/stories/controllers/story_list_controller.js similarity index 57% rename from src/app/teams/controllers/teams_list_controller.js rename to src/app/stories/controllers/story_list_controller.js index 93f6d55c..7e3180c7 100644 --- a/src/app/teams/controllers/teams_list_controller.js +++ b/src/app/stories/controllers/story_list_controller.js @@ -15,16 +15,26 @@ */ /** - * The team list controller handles discovery for all teams, including - * search. Note that it is assumed that we implemented a search (inclusive), - * rather than a browse (exclusive) approach. + * Controller for our story list. */ -angular.module('sb.teams').controller('TeamsListController', - function ($scope, Team) { +angular.module('sb.story').controller('StoryListController', + function ($scope, $modal, NewStoryService, Story) { 'use strict'; + $scope.newStory = function () { + NewStoryService.showNewStoryModal(); + }; + + // Variables and methods available to the template... - $scope.teams = []; + function resetScope() { + $scope.storyCount = 0; + $scope.storyOffset = 0; + $scope.storyLimit = 10; + $scope.stories = []; + $scope.error = {}; + } + $scope.searchQuery = ''; $scope.isSearching = false; @@ -33,19 +43,26 @@ angular.module('sb.teams').controller('TeamsListController', */ $scope.search = function () { // Clear the scope and set the progress flag. - $scope.error = {}; + resetScope(); $scope.isSearching = true; - $scope.teams = []; - // Execute the team search. - Team.search( - // Enable this once the API's there, mocks don't support - // searches yet - {/* q: $scope.searchQuery || '' */}, - function (result) { + // Execute the story query. + Story.query( + // Enable this once the API accepts search queries. + {}, + function (result, headers) { + + // Extract metadata from returned headers. + var storyCount = headers('X-List-Total') || result.length; + var storyOffset = headers('X-List-Offset') || 0; + var storyLimit = headers('X-List-Limit') || result.length; + // Successful search results, apply the results to the // scope and unset our progress flag. - $scope.teams = result; + $scope.storyCount = storyCount; + $scope.storyOffset = storyOffset; + $scope.storyLimit = storyLimit; + $scope.stories = result; $scope.isSearching = false; }, function (error) { @@ -58,5 +75,6 @@ angular.module('sb.teams').controller('TeamsListController', }; // Initialize the view with a default search. + resetScope(); $scope.search(); }); diff --git a/src/app/stories/controllers/story_modal_controller.js b/src/app/stories/controllers/story_modal_controller.js new file mode 100644 index 00000000..35de1b24 --- /dev/null +++ b/src/app/stories/controllers/story_modal_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. + */ + +/** + * Controller for the "new story" modal popup. + */ +angular.module('sb.story').controller('StoryModalController', + function ($scope, $modalInstance, params, Project, Story) { + 'use strict'; + + $scope.story = new Story(); + $scope.projects = Project.query({}, + function (results) { + if (params.projectId !== null) { + for (var i = 0; i < results.length; i++) { + var project = results[i]; + if (project.id === params.projectId) { + $scope.project = project; + return; + } + } + } + }); + + $scope.save = function () { + $scope.story.$create(function () { + $modalInstance.dismiss('success'); + }); + }; + + $scope.close = function () { + $modalInstance.dismiss('cancel'); + }; + }); diff --git a/src/app/stories/module.js b/src/app/stories/module.js new file mode 100644 index 00000000..65ef29ef --- /dev/null +++ b/src/app/stories/module.js @@ -0,0 +1,71 @@ +/* + * 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. + */ + +/** + * The Storyboard story submodule handles most activity surrounding the + * creation and management of stories, their tasks, and comments. + */ +angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util']) + .config(function ($stateProvider, $urlRouterProvider) { + 'use strict'; + + // URL Defaults. + $urlRouterProvider.when('/story', '/story/list'); + $urlRouterProvider.when('/story/{id:[0-9]+}', + function ($match) { + return '/story/' + $match.id + '/overview'; + }); + $urlRouterProvider.when('/story/{storyId:[0-9]+}/task', + function ($match) { + return '/story/' + $match.storyId + '/overview'; + }); + $urlRouterProvider.when('/story/{storyId:[0-9]+}/task/{taskId:[0-9]+}', + function ($match) { + return '/story/' + $match.storyId + + '/task/' + $match.taskId; + }); + + // Set our page routes. + $stateProvider + .state('story', { + abstract: true, + url: '/story', + template: '
' + }) + .state('story.list', { + url: '/list', + templateUrl: 'app/templates/story/list.html', + controller: 'StoryListController' + }) + .state('story.detail', { + url: '/{storyId:[0-9]+}', + abstract: true, + templateUrl: 'app/templates/story/detail.html', + controller: 'StoryDetailController' + }) + .state('story.detail.overview', { + url: '/overview', + templateUrl: 'app/templates/story/overview.html' + }) + .state('story.detail.edit', { + url: '/edit', + templateUrl: 'app/templates/story/edit.html' + }) + .state('story.detail.delete', { + url: '/delete', + templateUrl: 'app/templates/story/delete.html' + }); + }); diff --git a/src/app/stories/services/new_story_service.js b/src/app/stories/services/new_story_service.js new file mode 100644 index 00000000..e23e2c82 --- /dev/null +++ b/src/app/stories/services/new_story_service.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. + */ + + +angular.module('sb.story') + .factory('NewStoryService', function ($modal, $log) { + 'use strict'; + + return { + showNewStoryModal: function (projectId) { + + var modalInstance = $modal.open( + { + templateUrl: 'app/templates/story/new.html', + controller: 'StoryModalController', + resolve: { + params: function () { + return { + projectId: projectId || null + }; + } + } + } + ); + + modalInstance.result.then(function () { + // Do nothing. + }, function () { + $log.info('Modal dismissed at: ' + new Date()); + }); + } + }; + } +); diff --git a/src/app/storyboard/controllers/header_controller.js b/src/app/storyboard/controllers/header_controller.js new file mode 100644 index 00000000..c278f79b --- /dev/null +++ b/src/app/storyboard/controllers/header_controller.js @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Controller for our application header. + */ +angular.module('storyboard').controller('HeaderController', + function ($scope, $modal, NewStoryService) { + 'use strict'; + + $scope.newStory = function () { + NewStoryService.showNewStoryModal(); + }; + }); diff --git a/src/app/storyboard/controllers/home_controller.js b/src/app/storyboard/controllers/home_controller.js new file mode 100644 index 00000000..b020dc77 --- /dev/null +++ b/src/app/storyboard/controllers/home_controller.js @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Controller for our home(index) page, currently just a placeholder. + */ +angular.module('storyboard').controller('HomeController', + function ($scope, $modal, NewStoryService) { + 'use strict'; + + $scope.newStory = function () { + NewStoryService.showNewStoryModal(); + }; + }); diff --git a/src/app/storyboard.js b/src/app/storyboard/module.js similarity index 86% rename from src/app/storyboard.js rename to src/app/storyboard/module.js index 1f3658df..4aa039cd 100644 --- a/src/app/storyboard.js +++ b/src/app/storyboard/module.js @@ -1,11 +1,11 @@ /* - * Copyright (c) 2013 Hewlett-Packard Development Company, L.P. + * 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 + * 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 @@ -24,7 +24,7 @@ */ angular.module('storyboard', [ 'sb.services', 'sb.templates', 'sb.pages', 'sb.projects', 'sb.auth', - 'sb.teams', 'sb.project_groups', 'ui.router'] + 'sb.story', 'ui.router', 'ui.bootstrap'] ) .config(function ($provide, $stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) { @@ -40,7 +40,8 @@ angular.module('storyboard', $stateProvider .state('index', { url: '/', - templateUrl: 'app/templates/index.html' + templateUrl: 'app/templates/index.html', + controller: 'HomeController' }); // Attach common request headers out of courtesy to the API diff --git a/src/app/teams/module.js b/src/app/teams/module.js deleted file mode 100644 index 6cd76319..00000000 --- a/src/app/teams/module.js +++ /dev/null @@ -1,40 +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. - */ - -/** - * The Storyboard team submodule handles most activity surrounding the - * creation and management of project teams. - */ -angular.module('sb.teams', ['ui.router', 'sb.services', 'sb.util']) - .config(function ($stateProvider, $urlRouterProvider) { - 'use strict'; - - // URL Defaults. - $urlRouterProvider.when('/teams', '/teams/list'); - - // Set our page routes. - $stateProvider - .state('teams', { - abstract: true, - url: '/teams', - template: '
' - }) - .state('teams.list', { - url: '/list', - templateUrl: 'app/templates/teams/list.html', - controller: 'TeamsListController' - }); - }); diff --git a/src/app/templates/footer.html b/src/app/templates/footer.html index f230ea0b..c439bdba 100644 --- a/src/app/templates/footer.html +++ b/src/app/templates/footer.html @@ -21,15 +21,17 @@ About -
-