Replace "New Story" button in the top bar with a dropdown menu

Now there are more things than just stories for normal users
to create (boards and worklists too), a dropdown menu is sensible
here rather than adding a number of buttons. This also adds a
way for superusers to easily create projects and project groups
from any page.

Change-Id: Icb192e64d4bae8af22b01960ecac3c00d092da85
This commit is contained in:
Adam Coldrick
2015-10-07 10:40:17 +00:00
parent 0864a6e8d2
commit 6eb0bcfdbd
7 changed files with 554 additions and 11 deletions

View File

@@ -0,0 +1,125 @@
/*
* Copyright (c) 2015 Codethink Limited
*
* 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 board" modal popup.
*/
angular.module('sb.board').controller('AddBoardController',
function ($scope, $modalInstance, $state, params, Board, Project,
Worklist, $q, BoardHelper) {
'use strict';
/**
* Create the new board.
*/
function saveBoard() {
$scope.board.$create(
function (result) {
$modalInstance.dismiss('success');
$state.go('sb.board.detail', {boardID: result.id});
}
);
}
/**
* Return a function which adds a "lane" to the board in the given
* position. This lane is just a reference to the worklist used to
* represent it.
*/
function addLaneDetails(position) {
return function(lane) {
$scope.board.lanes.push({
board_id: $scope.board.id,
list_id: lane.id,
position: position
});
};
}
/**
* Create worklists to represent the lanes of the board, then save
* the board.
*/
function saveBoardWithLanes() {
$scope.board.lanes = [];
var lanePromises = [];
for (var i = 0; i < $scope.lanes.length; i++) {
var lane = $scope.lanes[i];
var addLane = addLaneDetails(i);
lane.project_id = $scope.board.project_id;
lane.private = $scope.board.private;
lanePromises.push(lane.$create(addLane));
}
$q.all(lanePromises).then(saveBoard);
}
/**
* Saves the board, and any worklists created to serve as lanes.
*/
$scope.save = function() {
if ($scope.lanes.length > 0) {
saveBoardWithLanes();
} else {
saveBoard();
}
};
/**
* Close this modal without saving.
*/
$scope.close = function() {
$modalInstance.dismiss('cancel');
};
/**
* Add a lane.
*/
$scope.addLane = function() {
$scope.lanes.push(new Worklist({
title: '',
editing: true
}));
};
/**
* Remove a lane.
*/
$scope.removeLane = function(lane) {
var idx = $scope.lanes.indexOf(lane);
$scope.lanes.splice(idx, 1);
};
/**
* Toggle editing of a lane title.
*/
$scope.toggleEdit = function(lane) {
lane.editing = !lane.editing;
};
/**
* Config for the lanes sortable.
*/
$scope.lanesSortable = {
dragMove: BoardHelper.maybeScrollContainer('new-board')
};
// Create a blank Board to begin with.
$scope.lanes = [];
$scope.board = new Board({
title: '',
lanes: []
});
});

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2015 Codethink Limited.
*
* 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.board').factory('NewBoardService',
function ($modal, $log, Session, SessionModalService) {
'use strict';
return {
showNewBoardModal: function (userId) {
if (!Session.isLoggedIn()) {
return SessionModalService.showLoginRequiredModal();
} else {
var modalInstance = $modal.open(
{
size: 'lg',
templateUrl: 'app/boards/template/new.html',
controller: 'AddBoardController',
resolve: {
params: function () {
return {
userId: userId || null
};
}
}
}
);
// Return the modal's promise.
return modalInstance.result;
}
}
};
}
);

View File

@@ -0,0 +1,118 @@
<!--
~ Copyright (c) 2015 Codethink Limited
~
~ 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="panel panel-default">
<div class="panel-heading">
<button type="button" class="close" aria-hidden="true"
ng-click="close()">&times;</button>
<h3 class="panel-title">New Board</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="worklistForm">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">
Title
</label>
<div class="col-sm-10">
<input id="title"
focus
type="text"
class="form-control"
ng-model="board.title"
required
maxlength="100"
placeholder="Board title">
</div>
</div>
<div class="form-group">
<label for="description"
class="col-sm-2 control-label">
Description
</label>
<div class="col-sm-10">
<textarea id="description"
class="form-control"
ng-model="board.description"
msd-elastic
placeholder="A short description of the board's purpose.">
</textarea>
</div>
</div>
<div class="form-group">
<label for="private" class="col-sm-2 control-label">
Private
</label>
<div class="col-sm-10 checkbox">
<input id="private"
type="checkbox"
class="modal-checkbox"
ng-model="board.private"
/>
</div>
</div>
</form>
</div>
</div>
<div class="kanban-board"
id="new-board"
ng-model="lanes"
as-sortable="lanesSortable">
<div class="kanban-lane"
ng-repeat="worklist in lanes"
as-sortable-item>
<span ng-if="!worklist.editing">
<i class="fa fa-bars" as-sortable-item-handle></i>
&nbsp;
<a ng-click="toggleEdit(worklist)">
{{worklist.title}}
<i class="fa fa-pencil"></i>
</a>
<button type="button" class="close" title="Remove"
ng-click="removeLane(worklist)">
&times;
</button>
</span>
<input class="form-control"
type="text"
ng-model="worklist.title"
placeholder="Lane Title"
focus
ng-if="worklist.editing"
ng-blur="toggleEdit(worklist)" />
</div><div class="kanban-lane-clickable"
ng-click="addLane()">
Add lane
</div>
</div>
<div class="row">
<div class="col-xs-12 text-right">
<button type="button"
class="btn btn-primary"
ng-click="save()">
Save Changes
</button>
<button type="button"
ng-click="close()"
class="btn btn-default">
Cancel
</button>
</div>
</div>
</div>
</div>

View File

@@ -19,9 +19,10 @@
* and search box.
*/
angular.module('storyboard').controller('HeaderController',
function ($q, $scope, $rootScope, $state, NewStoryService, Session,
SessionState, CurrentUser, Criteria, Notification, Priority,
Project, Story, ProjectGroup) {
function ($q, $scope, $rootScope, $state, $modal, NewStoryService,
Session, SessionState, CurrentUser, Criteria, Notification,
Priority, Project, Story, ProjectGroup, NewWorklistService,
NewBoardService, SessionModalService) {
'use strict';
function resolveCurrentUser() {
@@ -49,11 +50,66 @@ angular.module('storyboard').controller('HeaderController',
NewStoryService.showNewStoryModal()
.then(function (story) {
// On success, go to the story detail.
$scope.showMobileNewMenu = false;
$state.go('sb.story.detail', {storyId: story.id});
}
);
};
/**
* Create a new worklist.
*/
$scope.newWorklist = function () {
NewWorklistService.showNewWorklistModal()
.then(function (worklist) {
// On success, go to the worklist detail.
$scope.showMobileNewMenu = false;
$state.go('sb.worklist.detail',
{worklistID: worklist.id});
}
);
};
/**
* Create a new board.
*/
$scope.newBoard = function () {
NewBoardService.showNewBoardModal()
.then(function (board) {
// On success, go to the board detail.
$scope.showMobileNewMenu = false;
$state.go('sb.board.detail', {boardID: board.id});
}
);
};
/**
* Create a new project-group.
*/
$scope.newProjectGroup = function () {
$scope.modalInstance = $modal.open(
{
templateUrl: 'app/project_group/template/new.html',
controller: 'ProjectGroupNewController'
});
$scope.modalInstance.result.then(function (projectGroup) {
// On success, go to the project group detail.
$scope.showMobileNewMenu = false;
$state.go(
'sb.project_group_detail',
{id: projectGroup.id}
);
});
};
/**
* Show modal informing the user login is required.
*/
$scope.showLoginRequiredModal = function() {
SessionModalService.showLoginRequiredModal();
};
/**
* Log out the user.
*/

View File

@@ -39,12 +39,49 @@
<ul class="nav navbar-nav">
<li>&nbsp;&nbsp;</li>
<li>
<li class="dropdown" dropdown ng-show="isLoggedIn">
<button type="button"
class="btn btn-primary navbar-btn"
ng-click="newStory()">
class="btn btn-primary navbar-btn dropdown-toggle"
dropdown-toggle>
<i class="fa fa-plus-circle"></i>
New Story
Create New...
<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu">
<li>
<a href ng-click="newStory()">
Story
</a>
</li>
<li>
<a href ng-click="newWorklist()">
Worklist
</a>
</li>
<li>
<a href ng-click="newBoard()">
Board
</a>
</li>
<li ng-show="currentUser.is_superuser">
<a href="#!/project/new">
Project
</a>
</li>
<li ng-show="currentUser.is_superuser">
<a href ng-click="newProjectGroup()">
Project Group
</a>
</li>
</ul>
</li>
<li ng-show="!isLoggedIn">
<button type="button"
class="btn btn-primary navbar-btn dropdown-toggle"
ng-click="showLoginRequiredModal()">
<i class="fa fa-plus-circle"></i>
Create New...
<i class="fa fa-caret-down"></i>
</button>
</li>
</ul>
@@ -118,14 +155,55 @@
ng-hide="isLoggedIn">
<i class="fa fa-sign-in"></i>
</a>
<a href="#" ng-click="newStory()"
class="btn btn-sm btn-primary navbar-btn">
<button type="button"
ng-click="showMobileNewMenu = !showMobileNewMenu"
class="btn btn-sm btn-primary navbar-btn"
ng-show="isLoggedIn">
<i class="fa fa-plus-circle"></i>
New Story
</a>
Create New...
</button>
<button type="button"
class="btn btn-primary navbar-btn dropdown-toggle"
ng-click="showLoginRequiredModal()"
ng-show="!isLoggedIn">
<i class="fa fa-plus-circle"></i>
Create New...
<i class="fa fa-caret-down"></i>
</button>
<a class="navbar-brand" href="#!/">StoryBoard</a>
</div>
<div collapse="!showMobileNewMenu" id="mobile-new-menu">
<ul class="nav navbar-nav">
<li>
<a href ng-click="newStory()">
Story
</a>
</li>
<li>
<a href ng-click="newWorklist()">
Worklist
</a>
</li>
<li>
<a href ng-click="newBoard()">
Board
</a>
</li>
<li ng-show="currentUser.is_superuser">
<a href="#!/project/new"
ng-click="showMobileNewMenu = false">
Project
</a>
</li>
<li ng-show="currentUser.is_superuser">
<a href ng-click="newProjectGroup()">
Project Group
</a>
</li>
</ul>
</div>
<div collapse="!showMobileMenu" id="mobile-dropdown-menu">
<ul class="nav navbar-nav">
<li active-path="^\/profile\/preferences*">

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2015 Codethink Limited.
*
* 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.worklist').factory('NewWorklistService',
function ($modal, $log, Session, SessionModalService) {
'use strict';
return {
showNewWorklistModal: function (userId) {
if (!Session.isLoggedIn()) {
return SessionModalService.showLoginRequiredModal();
} else {
var modalInstance = $modal.open(
{
templateUrl: 'app/worklists/template/new.html',
controller: 'AddWorklistController',
resolve: {
params: function () {
return {
userId: userId || null
};
}
}
}
);
// Return the modal's promise.
return modalInstance.result;
}
}
};
}
);

View File

@@ -0,0 +1,71 @@
<!--
~ Copyright (c) 2015 Codethink Limited
~
~ 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="panel panel-default">
<div class="panel-heading">
<button type="button" class="close" aria-hidden="true"
ng-click="close()">&times;</button>
<h3 class="panel-title">New Worklist</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="worklistForm">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
Title
</label>
<div class="col-sm-10">
<input id="name"
focus
type="text"
class="form-control"
ng-model="worklist.title"
required
maxlength="100"
placeholder="Worklist Title">
</div>
</div>
<div class="form-group">
<label for="private" class="col-sm-2 control-label">
Private
</label>
<div class="col-sm-10 checkbox">
<input id="private"
type="checkbox"
class="modal-checkbox"
ng-model="worklist.private"
/>
</div>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-xs-12 text-right">
<button type="button"
class="btn btn-primary"
ng-click="save()">
Save Changes
</button>
<button type="button"
ng-click="close()"
class="btn btn-default">
Cancel
</button>
</div>
</div>
</div>
</div>