Some project ui/ux updates

- Collapsed edit/list/overview project into one view much like story
detail.
- Removed the "Project detail" prefix from the detail page.
- Removed the "Project name" header from the list.
- Removed extraneous views.
- Added a permission resolver that only returns the value of a
permission rather than blocking the route resolution.

Change-Id: I026aad56d3be2882e5a9b99a8c738634e6c6a1dc
This commit is contained in:
Michael Krotscheck 2014-04-29 16:00:52 -07:00
parent 235444abb6
commit 16f4ee00c7
9 changed files with 201 additions and 332 deletions

View File

@ -49,5 +49,28 @@ angular.module('sb.auth').constant('PermissionResolver',
return deferred.promise;
};
},
/**
* Resolves the value of the provided permission.
*/
resolvePermission: function (permName) {
'use strict';
return function ($q, $log, PermissionManager) {
var deferred = $q.defer();
PermissionManager.resolve(permName).then(
function (value) {
deferred.resolve(value);
},
function () {
deferred.resolve(false);
}
);
return deferred.promise;
};
}
});

View File

@ -28,7 +28,8 @@
* seconds. 3 is preferable.
*/
angular.module('sb.projects').controller('ProjectDetailController',
function ($scope, $state, $stateParams, Project, Story) {
function ($scope, $state, $stateParams, Project, Story, Session,
isSuperuser) {
'use strict';
// Parse the ID
@ -43,15 +44,6 @@ 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.
*
@ -84,40 +76,50 @@ angular.module('sb.projects').controller('ProjectDetailController',
$scope.isUpdating = false;
}
// Sanity check, do we actually have an ID? (zero is falsy)
if (!id && id !== 0) {
// We should never reach this, however that logic lives outside
// of this controller which could be unknowningly refactored.
$scope.error = {
error: true,
error_code: 404,
error_message: 'You did not provide a valid ID.'
};
/**
* Resets our loading flags.
*/
function handleServiceSuccess() {
$scope.isLoading = false;
} else {
// We've got an ID, so let's load it...
$scope.isUpdating = false;
}
/**
* Load the project
*/
function loadProject() {
Project.read(
{'id': id},
function (result) {
// We've got a result, assign it to the view and unset our
// loading flag.
$scope.project = result;
$scope.isLoading = false;
},
handleServiceError
);
// Load the count of stories while we're at it...
Story.query({project_id: id},
function (result, headers) {
// Only extract the total header...
$scope.projectStoryCount =
headers('X-List-Total') || result.length;
handleServiceSuccess();
},
handleServiceError
);
}
/**
* Toggles the form back.
*/
$scope.cancel = function () {
loadProject();
$scope.showEditForm = false;
};
/**
* Toggle/display the edit form
*/
$scope.toggleEditMode = function () {
if (isSuperuser) {
$scope.showEditForm = !$scope.showEditForm;
} else {
$scope.showEditForm = false;
}
};
/**
* Scope method, invoke this when you want to update the project.
*/
@ -131,25 +133,12 @@ angular.module('sb.projects').controller('ProjectDetailController',
function () {
// Unset our loading flag and navigate to the detail view.
$scope.isUpdating = false;
$state.go('project.detail', {id: $scope.project.id});
$scope.showEditForm = false;
handleServiceSuccess();
},
handleServiceError
);
};
/**
* Delete method.
*/
$scope.remove = function () {
// Set our progress flags and clear previous error conditions.
$scope.isUpdating = true;
$scope.error = {};
$scope.project.$delete(
function () {
$state.go('project.list');
},
handleServiceError
);
};
loadProject();
});

View File

@ -19,24 +19,24 @@
* creation and management of projects.
*/
angular.module('sb.projects',
['ui.router', 'sb.services', 'sb.util', 'sb.auth'])
['ui.router', 'sb.services', 'sb.util', 'sb.auth'])
.config(function ($stateProvider, $urlRouterProvider, SessionResolver,
PermissionResolver) {
'use strict';
// 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
.state('project', {
abstract: true,
url: '/project',
template: '<div ui-view></div>'
template: '<div ui-view></div>',
resolve: {
isSuperuser: PermissionResolver
.resolvePermission('is_superuser', true)
}
})
.state('project.list', {
url: '/list',
@ -44,38 +44,10 @@ angular.module('sb.projects',
controller: 'ProjectListController'
})
.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',
resolve: {
isLoggedIn: SessionResolver.requireLoggedIn,
isSuperuser: PermissionResolver
.requirePermission('is_superuser', true)
}
}).
state('project.detail.delete', {
url: '/delete',
templateUrl: 'app/templates/project/delete.html',
resolve: {
isLoggedIn: SessionResolver.requireLoggedIn,
isSuperuser: PermissionResolver
.requirePermission('is_superuser', true)
}
})
.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',

View File

@ -1,34 +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.
-->
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<h2 class="text-danger text-center">
Are you certain that you want to delete this project?
</h2>
<p class="text-center lead">
This will set the project to a "deleted" state, and any stories and
tasks will no longer be visible.
</p>
<div class="text-center">
<a href="" class="btn btn-danger" ng-click="remove()">
Remove this project
</a>
</div>
</div>
</div>

View File

@ -13,47 +13,143 @@
~ License for the specific language governing permissions and limitations
~ under the License.
-->
<div class="container" ng-show="isLoading">
<div class="container">
<div class="row">
<p class="text-center">
<i class="fa fa-refresh fa-spin fa-lg"></i>
</p>
<div ng-include
class="col-xs-12"
src="'/inline/project_detail.html'"
ng-hide="showEditForm">
</div>
<div ng-include
src="'/inline/project_detail_form.html'"
ng-show="showEditForm">
</div>
<div ng-include src="'/inline/story_list.html'"></div>
</div>
</div>
<div class="container" ng-hide="isLoading">
<div class="row">
<h1 class="no-border no-margin-bottom">Project detail:
<!-- Template for the header and description -->
<script type="text/ng-template" id="/inline/project_detail.html">
<h1>
<span ng-show="project.name">
{{project.name}}
</h1>
<ul class="nav nav-tabs nav-tabs-down nav-thick">
<li active-path="^\/project\/[0-9]+\/overview.*">
<a href="#!/project/{{project.id}}/overview">
Overview
</a>
</li>
<li active-path="^\/project\/[0-9]+\/stories.*">
<a href="#!/project/{{project.id}}/stories">
Stories
<span ng-show="!!projectStoryCount">
({{projectStoryCount}})
</span>
</a>
</li>
<li active-path="^\/project\/[0-9]+\/edit.*"
permission="is_superuser">
<a href="#!/project/{{project.id}}/edit">
Edit
</a>
</li>
<li active-path="^\/project\/[0-9]+\/delete.*"
permission="is_superuser">
<a href="#!/project/{{project.id}}/delete">
Delete
</a>
</li>
</ul>
<br/>
</span>
<em ng-hide="project.name" class="text-muted">
No title
</em>
<small ng-show="isLoggedIn">
<a href="" ng-click="toggleEditMode()" permission="is_superuser">
<i class="fa fa-pencil"></i>
</a>
</small>
</h1>
<p>
<span ng-show="project.description"
class="honor-carriage-return">{{project.description}}
</span>
<em ng-hide="project.description" class="text-muted">
No description provided
</em>
</p>
</script>
<!-- Template for the header and description -->
<script type="text/ng-template" id="/inline/project_detail_form.html">
<br/>
<form name="projectForm">
<div class="form-group">
<input type="text"
class="form-control"
ng-model="project.name"
required
ng-disabled="isUpdating"
placeholder="Project Name">
</input>
</div>
<div class="form-group">
<textarea placeholder="Enter a project description here"
class="form-control"
rows="3"
required
ng-disabled="isUpdating"
ng-model="project.description">
</textarea>
</div>
<div class="clearfix">
<div class="pull-right">
<div class="btn" ng-show="isUpdating">
<i class="fa fa-spinner fa-lg fa-spin"></i>
</div>
<button type="button"
class="btn btn-primary"
ng-click="update()"
ng-disabled="!projectForm.$valid">
Save
</button>
<button type="button"
class="btn btn-default"
ng-click="cancel()">
Cancel
</button>
</div>
</div>
</form>
<hr/>
</script>
<!-- Template for the task list -->
<script type="text/ng-template" id="/inline/story_list.html">
<div ng-controller="ProjectStoryListController">
<div class="col-xs-12">
<a href=""
class="btn btn-link pull-right"
ng-click="newStory()"
ng-show="isLoggedIn">
<i class="fa fa-plus"></i>
Add story
</a>
<ul class="nav nav-tabs clearfix">
<li ng-class="{'active': filter == 'active'}">
<a href=""
ng-click="setFilter('active')">Active</a>
</li>
<li ng-class="{'active': filter == 'merged'}">
<a href=""
ng-click="setFilter('merged')">Merged</a>
</li>
<li ng-class="{'active': filter == 'invalid'}">
<a href=""
ng-click="setFilter('invalid')">Invalid</a>
</li>
</ul>
<table class="table table-striped"
ng-hide="isSearching || stories.length == 0">
<tbody>
<tr ng-repeat="story in stories"
ng-controller="StoryListItemController"
ng-include="'app/templates/story/story_list_item.html'">
</tr>
</tbody>
</table>
<div ng-show="isSearching">
<hr/>
<p class="text-center">
<i class="fa fa-refresh fa-spin fa-lg"></i>
</p>
</div>
<p ng-show="!isSearching && stories.length == 0"
class="text-center text-warning">
<br/>
<em> We were unable to find any stories.
Perhaps you would like to create one?</em>
</p>
</div>
</div>
<div class="row" ui-view></div>
</div>
</script>

View File

@ -1,64 +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.
-->
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="projectForm">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
Project Name:
</label>
<div class="col-sm-10">
<input id="name"
type="text"
class="form-control"
ng-model="project.name"
required
placeholder="Project Name">
</div>
</div>
<div class="form-group">
<label for="description"
class="col-sm-2 control-label">
Project Description
</label>
<div class="col-sm-10">
<textarea id="description"
class="form-control"
ng-model="project.description"
required
placeholder="A brief project description">
</textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button"
ng-click="update()"
class="btn btn-primary"
ng-disabled="!projectForm.$valid">
Save Changes
</button>
<a href="#!/project/list"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</form>
</div>
</div>

View File

@ -46,9 +46,7 @@
ng-hide="isSearching || projects.length == 0">
<thead>
<tr>
<th class="col-sm-10">
<small>Project Name</small>
</th>
<th class="col-sm-10">&nbsp;</th>
</tr>
</thead>
<tbody>

View File

@ -1,28 +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.
-->
<div class="row">
<div class="col-xs-12">
<p ng-show="project.description"
class="honor-carriage-return">{{project.description}}
</p>
<p ng-hide="project.description"
class="text-muted text-center">
No description available.
</p>
</div>
</div>

View File

@ -1,83 +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.
-->
<div class="col-sm-3 col-xs-10">
<div class="btn-group btn-group-justified">
<div class="form-group has-feedback has-feedback-no-label">
<input type="text"
class="form-control input-sm"
placeholder="Search Stories"
ng-disabled="isSearching"
ng-enter="search()"
ng-model="searchQuery"/>
<span class="fa fa-search form-control-feedback"
ng-hide="isSearching"></span>
<span class="fa fa-refresh fa-spin form-control-feedback"
ng-show="isSearching"></span>
</div>
</div>
</div>
<div class="col-sm-9 col-xs-2">
<a href=""
ng-click="newStory()"
class="pull-right btn btn-default btn-sm">
<i class="fa fa-plus"></i>
<span class="hidden-xs">New Story</span>
</a>
</div>
<div class="col-xs-12">
<ul class="nav nav-tabs clearfix">
<li ng-class="{'active': filter == 'active'}">
<a href=""
ng-click="setFilter('active')">Active</a>
</li>
<li ng-class="{'active': filter == 'merged'}">
<a href=""
ng-click="setFilter('merged')">Merged</a>
</li>
<li ng-class="{'active': filter == 'invalid'}">
<a href=""
ng-click="setFilter('invalid')">Invalid</a>
</li>
</ul>
<table class="table table-striped"
ng-hide="isSearching || stories.length == 0">
<tbody>
<tr ng-repeat="story in stories"
ng-controller="StoryListItemController"
ng-include="'app/templates/story/story_list_item.html'">
</tr>
</tbody>
</table>
</div>
<div ng-show="!isSearching && stories.length == 0"
class="col-sm-12 text-center text-warning">
<br/>
<p> We were unable to find any stories in this project.
Perhaps you would like to create one?</p>
</div>
<div class="col-sm-12">
<div ng-show="isSearching">
<br/>
<p class="text-center">
<i class="fa fa-refresh fa-spin fa-lg"></i>
</p>
</div>
</div>