Allow teams to be added to the permission list for private stories

This commit adds a way to add teams to the permission list for private
stories. This makes it easy to use for stories that should only be
visible to a given set of people; the members of that set can now be
managed by the teams UI rather than on a per-story basis.

Change-Id: I7d3adde5d6cf9b4d5b7e63df06d2266f7b824666
Depends-On: I16a7451e8c24da352357abb0590d0ace5b4e77f4
This commit is contained in:
Adam Coldrick 2016-09-14 16:04:03 +00:00 committed by Adam Coldrick
parent 661b1b961f
commit a33bb27f10
6 changed files with 203 additions and 92 deletions

View File

@ -123,6 +123,7 @@
<td colspan="2"> <td colspan="2">
<input id="user" <input id="user"
type="text" type="text"
autocomplete="off"
placeholder="Click to add a user" placeholder="Click to add a user"
ng-model="asyncUser" ng-model="asyncUser"
typeahead-wait-ms="200" typeahead-wait-ms="200"

View File

@ -106,6 +106,7 @@
<td colspan="2"> <td colspan="2">
<input id="user" <input id="user"
type="text" type="text"
autocomplete="off"
placeholder="Click to add a user" placeholder="Click to add a user"
ng-model="asyncUser" ng-model="asyncUser"
typeahead-wait-ms="200" typeahead-wait-ms="200"

View File

@ -24,7 +24,7 @@ angular.module('sb.story').controller('StoryDetailController',
Story, Project, Branch, creator, tasks, Task, DSCacheFactory, Story, Project, Branch, creator, tasks, Task, DSCacheFactory,
User, $q, storyboardApiBase, SessionModalService, moment, User, $q, storyboardApiBase, SessionModalService, moment,
$document, $anchorScroll, $timeout, $location, currentUser, $document, $anchorScroll, $timeout, $location, currentUser,
enableEditableComments, Tags, worklists) { enableEditableComments, Tags, worklists, Team) {
'use strict'; 'use strict';
var pageSize = Preference.get('story_detail_page_size'); var pageSize = Preference.get('story_detail_page_size');
@ -532,51 +532,82 @@ angular.module('sb.story').controller('StoryDetailController',
}; };
/** /**
* User typeahead search method. * User/team typeahead search method.
*/ */
$scope.searchUsers = function (value, array) { $scope.searchActors = function (value, users, teams) {
var userIds = array.map(function(user){return user.id;}); var userIds = users.map(function(user){return user.id;});
var teamIds = teams.map(function(team){return team.id;});
var deferred = $q.defer(); var deferred = $q.defer();
var usersDeferred = $q.defer();
var teamsDeferred = $q.defer();
User.browse({full_name: value, limit: 10}, User.browse({full_name: value, limit: 10},
function(searchResults) { function(searchResults) {
var results = []; var results = [];
angular.forEach(searchResults, function(result) { angular.forEach(searchResults, function(result) {
if (userIds.indexOf(result.id) === -1) { if (userIds.indexOf(result.id) === -1) {
result.name = result.full_name;
result.type = 'user';
results.push(result); results.push(result);
} }
}); });
deferred.resolve(results); usersDeferred.resolve(results);
} }
); );
Team.browse({name: value, limit: 10},
function(searchResults) {
var results = [];
angular.forEach(searchResults, function(result) {
if (teamIds.indexOf(result.id) === -1) {
result.type = 'team';
results.push(result);
}
});
teamsDeferred.resolve(results);
}
);
var searches = [teamsDeferred.promise, usersDeferred.promise];
$q.all(searches).then(function(searchResults) {
var results = [];
angular.forEach(searchResults, function(promise) {
angular.forEach(promise, function(result) {
results.push(result);
});
});
deferred.resolve(results);
});
return deferred.promise; return deferred.promise;
}; };
/** /**
* Formats the user name. * Add a new user or team to one of the permission levels.
*/ */
$scope.formatUserName = function (model) { $scope.addActor = function (model) {
if (!!model) { if (model.type === 'user') {
return model.name; $scope.story.users.push(model);
} else if (model.type === 'team') {
$scope.story.teams.push(model);
} }
return '';
}; };
/** /**
* Add a new user to one of the permission levels. * Remove a user from the permissions.
*/
$scope.addUser = function (model) {
$scope.story.users.push(model);
};
/**
* Remove a user from one of the permission levels.
*/ */
$scope.removeUser = function (model) { $scope.removeUser = function (model) {
var idx = $scope.story.users.indexOf(model); var idx = $scope.story.users.indexOf(model);
$scope.story.users.splice(idx, 1); $scope.story.users.splice(idx, 1);
}; };
/**
* Remove a team from the permissions.
*/
$scope.removeTeam = function(model) {
var idx = $scope.story.teams.indexOf(model);
$scope.story.teams.splice(idx, 1);
};
// ################################################################### // ###################################################################
// Task Management // Task Management
// ################################################################### // ###################################################################

View File

@ -19,7 +19,7 @@
*/ */
angular.module('sb.story').controller('StoryModalController', angular.module('sb.story').controller('StoryModalController',
function ($scope, $modalInstance, params, Project, Story, Task, User, function ($scope, $modalInstance, params, Project, Story, Task, User,
$q, CurrentUser) { Team, $q, CurrentUser) {
'use strict'; 'use strict';
var currentUser = CurrentUser.resolve(); var currentUser = CurrentUser.resolve();
@ -29,7 +29,8 @@ angular.module('sb.story').controller('StoryModalController',
currentUser.then(function(user) { currentUser.then(function(user) {
$scope.story = new Story({ $scope.story = new Story({
title: '', title: '',
users: [user] users: [user],
teams: []
}); });
}); });
@ -164,49 +165,80 @@ angular.module('sb.story').controller('StoryModalController',
}; };
/** /**
* User typeahead search method. * User/team typeahead search method.
*/ */
$scope.searchUsers = function (value, array) { $scope.searchActors = function (value, users, teams) {
var userIds = users.map(function(user){return user.id;});
var teamIds = teams.map(function(team){return team.id;});
var deferred = $q.defer(); var deferred = $q.defer();
var usersDeferred = $q.defer();
var teamsDeferred = $q.defer();
User.browse({full_name: value, limit: 10}, User.browse({full_name: value, limit: 10},
function(searchResults) { function(searchResults) {
var results = []; var results = [];
angular.forEach(searchResults, function(result) { angular.forEach(searchResults, function(result) {
if (array.indexOf(result.id) === -1) { if (userIds.indexOf(result.id) === -1) {
result.name = result.full_name;
result.type = 'user';
results.push(result); results.push(result);
} }
}); });
deferred.resolve(results); usersDeferred.resolve(results);
} }
); );
Team.browse({name: value, limit: 10},
function(searchResults) {
var results = [];
angular.forEach(searchResults, function(result) {
if (teamIds.indexOf(result.id) === -1) {
result.type = 'team';
results.push(result);
}
});
teamsDeferred.resolve(results);
}
);
var searches = [teamsDeferred.promise, usersDeferred.promise];
$q.all(searches).then(function(searchResults) {
var results = [];
angular.forEach(searchResults, function(promise) {
angular.forEach(promise, function(result) {
results.push(result);
});
});
deferred.resolve(results);
});
return deferred.promise; return deferred.promise;
}; };
/** /**
* Formats the user name. * Add a new user or team to one of the permission levels.
*/ */
$scope.formatUserName = function (model) { $scope.addActor = function (model) {
if (!!model) { if (model.type === 'user') {
return model.name; $scope.story.users.push(model);
} else if (model.type === 'team') {
$scope.story.teams.push(model);
} }
return '';
}; };
/** /**
* Add a new user to one of the permission levels. * Remove a user from the permissions.
*/
$scope.addUser = function (model) {
$scope.story.users.push(model);
};
/**
* Remove a user from one of the permission levels.
*/ */
$scope.removeUser = function (model) { $scope.removeUser = function (model) {
var idx = $scope.story.users.indexOf(model); var idx = $scope.story.users.indexOf(model);
$scope.story.users.splice(idx, 1); $scope.story.users.splice(idx, 1);
}; };
/**
* Remove a team from the permissions.
*/
$scope.removeTeam = function(model) {
var idx = $scope.story.teams.indexOf(model);
$scope.story.teams.splice(idx, 1);
};
}) })
; ;

View File

@ -187,31 +187,43 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-sm-offset-3" <div class="col-md-6 col-md-offset-3"
ng-show="story.private"> ng-show="story.private">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Users that can see this story</th> <th>Teams and Users that can see this story</th>
<th class="text-right"> <th class="text-right">
<small> <small>
<a href <a href
ng-click="showAddUser = !showAddUser"> ng-click="showAddUser = !showAddUser">
<i class="fa fa-plus" ng-if="!showAddUser"></i> <i class="fa fa-plus" ng-if="!showAddUser"></i>
<i class="fa fa-minus" ng-if="showAddUser"></i> <i class="fa fa-minus" ng-if="showAddUser"></i>
Add User Add Team or User
</a> </a>
</small> </small>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="team in story.teams">
<td colspan="2">
<i class="fa fa-sb-team"></i>
{{ team.name }}
<a class="close"
ng-click="removeTeam(team)"
ng-show="story.teams.length > 1 || story.users.length > 0">
&times;
</a>
</td>
</tr>
<tr ng-repeat="user in story.users"> <tr ng-repeat="user in story.users">
<td colspan="2"> <td colspan="2">
<i class="fa fa-sb-user"></i>
{{user.full_name}} {{user.full_name}}
<a class="close" <a class="close"
ng-click="removeUser(user)" ng-click="removeUser(user)"
ng-show="story.users.length > 1"> ng-show="story.users.length > 1 || story.teams.length > 0">
&times; &times;
</a> </a>
</td> </td>
@ -220,15 +232,16 @@
<td colspan="2"> <td colspan="2">
<input id="user" <input id="user"
type="text" type="text"
placeholder="Click to add a user" autocomplete="off"
placeholder="Click to add a team or user"
ng-model="asyncUser" ng-model="asyncUser"
typeahead-wait-ms="200" typeahead-wait-ms="200"
typeahead-editable="false" typeahead-editable="false"
typeahead="user as user.full_name for user in typeahead="actor as actor.name for actor in
searchUsers($viewValue, story.users)" searchActors($viewValue, story.users, story.teams)"
typeahead-loading="loadingUsers" typeahead-loading="loadingUsers"
typeahead-input-formatter="formatUserName($model)" typeahead-on-select="addActor($model)"
typeahead-on-select="addUser($model)" typeahead-template-url="/inline/actor-typeahead-item.html"
class="form-control input-sm" class="form-control input-sm"
/> />
</td> </td>
@ -275,6 +288,14 @@
</script> </script>
<script type="text/ng-template" id="/inline/actor-typeahead-item.html">
<a tabindex="-1">
<i ng-class="'fa fa-sb-' + match.model.type"></i>&nbsp;
<span ng-bind-html="match.model.name | typeaheadHighlight:query"></span>
</a>
</script>
<!-- Template for the list of relevant worklists --> <!-- Template for the list of relevant worklists -->
<script type="text/ng-template" id="/inline/worklists.html"> <script type="text/ng-template" id="/inline/worklists.html">
<table class="table table-striped table-condensed"> <table class="table table-striped table-condensed">

View File

@ -64,53 +64,69 @@
/> />
</div> </div>
</div> </div>
<div class="col-sm-6 col-sm-offset-3" <div class="row">
ng-show="story.private"> <div class="col-md-6 col-md-offset-3"
<table class="table table-striped"> ng-show="story.private">
<thead> <table class="table table-striped">
<tr> <thead>
<th>Users that can see this story</th> <tr>
<th class="text-right"> <th>Teams and Users that can see this story</th>
<small> <th class="text-right">
<a href <small>
ng-click="showAddUser = !showAddUser"> <a href
<i class="fa fa-plus" ng-if="!showAddUser"></i> ng-click="showAddUser = !showAddUser">
<i class="fa fa-minus" ng-if="showAddUser"></i> <i class="fa fa-plus" ng-if="!showAddUser"></i>
Add User <i class="fa fa-minus" ng-if="showAddUser"></i>
Add Team or User
</a>
</small>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="team in story.teams">
<td colspan="2">
<i class="fa fa-sb-team"></i>
{{ team.name }}
<a class="close"
ng-click="removeTeam(team)"
ng-show="story.teams.length > 1 || story.users.length > 0">
&times;
</a> </a>
</small> </td>
</th> </tr>
</tr> <tr ng-repeat="user in story.users">
</thead> <td colspan="2">
<tbody> <i class="fa fa-sb-user"></i>
<tr ng-repeat="user in story.users"> {{user.full_name}}
<td colspan="2"> <a class="close"
{{user.full_name}} ng-click="removeUser(user)"
<a class="close" ng-show="story.users.length > 1 || story.teams.length > 0">
ng-click="removeUser(user)"> &times;
&times; </a>
</a> </td>
</td> </tr>
</tr> <tr ng-show="showAddUser">
<tr ng-show="showAddUser"> <td colspan="2">
<td colspan="2"> <input id="user"
<input id="user" type="text"
type="text" autocomplete="off"
placeholder="Click to add a user" placeholder="Click to add a team or user"
ng-model="asyncUser" ng-model="asyncUser"
typeahead-wait-ms="200" typeahead-wait-ms="200"
typeahead-editable="false" typeahead-editable="false"
typeahead="user as user.full_name for user in typeahead="actor as actor.name for actor in
searchUsers($viewValue, story.users)" searchActors($viewValue, story.users, story.teams)"
typeahead-loading="loadingUsers" typeahead-loading="loadingUsers"
typeahead-input-formatter="formatUserName($model)" typeahead-on-select="addActor($model)"
typeahead-on-select="addUser($model)" typeahead-template-url="/inline/actor-typeahead-item.html"
class="form-control input-sm" class="form-control input-sm"
/> />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
</form> </form>
</div> </div>
@ -163,6 +179,15 @@
</div> </div>
</div> </div>
<script type="text/ng-template" id="/inline/actor-typeahead-item.html">
<a tabindex="-1">
<i ng-class="'fa fa-sb-' + match.model.type"></i>&nbsp;
<span ng-bind-html="match.model.name | typeaheadHighlight:query"></span>
</a>
</script>
<!-- Template for story metadata --> <!-- Template for story metadata -->
<script type="text/ng-template" id="/inline/task_row.html"> <script type="text/ng-template" id="/inline/task_row.html">
<td> <td>