Merge "Add a "New Story" view"
This commit is contained in:
commit
9a20857262
|
@ -0,0 +1,421 @@
|
|||
/*
|
||||
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
* Copyright (c) 2016 Codethink Ltd.
|
||||
* Copyright (c) 2017 Adam Coldrick
|
||||
*
|
||||
* 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 view.
|
||||
*
|
||||
* This view is accessible from /#!/story/new, and can understand the
|
||||
* following parameters in the URL:
|
||||
*
|
||||
* - title: The initial story title
|
||||
* - description: The initial content of the story description
|
||||
* - force_private: If truthy, the story will be forced to be private
|
||||
* (i.e. private with the option to change privacy hidden).
|
||||
* - private: If truthy, the story will begin set to private.
|
||||
* Unlike force_private, this allows the user to change to public.
|
||||
* - tags: Tags to set on the story. Can be given multiple times.
|
||||
* - team_id: A team ID to grant permissions for this story to. Can be
|
||||
* given multiple times.
|
||||
* - user_id: A user ID to grant permissions for this story to. Can be
|
||||
* given multiple times.
|
||||
*/
|
||||
angular.module('sb.story').controller('StoryNewController',
|
||||
function ($scope, $state, $stateParams, Story, Project, Branch, Tags,
|
||||
Task, Team, User, $q, storyboardApiBase, currentUser) {
|
||||
'use strict';
|
||||
|
||||
var story = new Story({
|
||||
title: $stateParams.title,
|
||||
description: $stateParams.description,
|
||||
private: !!$stateParams.private || !!$stateParams.force_private,
|
||||
users: [currentUser],
|
||||
teams: []
|
||||
});
|
||||
|
||||
// Convert the user_id and team_id parameters to arrays if needed
|
||||
if (!!$stateParams.team_id
|
||||
&& $stateParams.team_id.constructor !== Array) {
|
||||
$stateParams.team_id = [$stateParams.team_id];
|
||||
}
|
||||
if (!!$stateParams.user_id
|
||||
&& $stateParams.user_id.constructor !== Array) {
|
||||
$stateParams.user_id = [$stateParams.user_id];
|
||||
}
|
||||
// Populate the story's permission lists as requested
|
||||
angular.forEach($stateParams.team_id, function(team_id) {
|
||||
Team.get({team_id: team_id}).$promise.then(function(team) {
|
||||
story.teams.push(team);
|
||||
});
|
||||
});
|
||||
angular.forEach($stateParams.user_id, function(user_id) {
|
||||
User.get({id: user_id}).$promise.then(function(user) {
|
||||
story.users.push(user);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.story = story;
|
||||
|
||||
// Convert the tags parameter into an array if it isn't already
|
||||
if (!!$stateParams.tags && $stateParams.tags.constructor !== Array) {
|
||||
$stateParams.tags = [$stateParams.tags];
|
||||
}
|
||||
// List of tags to give the story
|
||||
$scope.tags = $stateParams.tags || [];
|
||||
|
||||
/**
|
||||
* If the force_private query parameter is set, then enforce story
|
||||
* privacy and hide the option from the user.
|
||||
**/
|
||||
$scope.forcePrivate = !!$stateParams.force_private;
|
||||
|
||||
/**
|
||||
* All tasks associated with this story, resolved in the state.
|
||||
*
|
||||
* @type {[Task]}
|
||||
*/
|
||||
$scope.projectNames = [];
|
||||
$scope.projects = {};
|
||||
$scope.tasks = [];
|
||||
|
||||
/**
|
||||
* UI flag for when we're initially loading the view.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
$scope.isLoading = true;
|
||||
|
||||
/**
|
||||
* UI view for when a change is round-tripping to the server.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
$scope.isUpdating = false;
|
||||
|
||||
$scope.isValid = function() {
|
||||
return !!$scope.tasks.length && !!$scope.story.title;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the tasks for the story.
|
||||
*/
|
||||
function createTasks(createdStory) {
|
||||
var resolvingTasks = $scope.tasks.length;
|
||||
angular.forEach($scope.tasks, function(task) {
|
||||
task.story_id = createdStory.id;
|
||||
task.$create(function() {
|
||||
resolvingTasks--;
|
||||
if (resolvingTasks === 0) {
|
||||
// Finished, navigate to the new story
|
||||
$state.go('sb.story.detail',
|
||||
{storyId: createdStory.id});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the story.
|
||||
*/
|
||||
$scope.createStory = function() {
|
||||
$scope.isUpdating = true;
|
||||
// Ensure story is private if force_private is set.
|
||||
if ($scope.forcePrivate) {
|
||||
$scope.story.private = true;
|
||||
}
|
||||
$scope.story.$create(function(createdStory) {
|
||||
var tagsController = new Story.TagsController({
|
||||
id: createdStory.id
|
||||
});
|
||||
if ($scope.tags.length > 0) {
|
||||
// Create tags for the story, then tasks
|
||||
tagsController.$update({tags: $scope.tags}, createTasks);
|
||||
} else {
|
||||
// Create tasks for the story
|
||||
createTasks(createdStory);
|
||||
}
|
||||
}, function() {
|
||||
$scope.isUpdating = false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* User/team typeahead search method.
|
||||
*/
|
||||
$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 usersDeferred = $q.defer();
|
||||
var teamsDeferred = $q.defer();
|
||||
|
||||
User.browse({full_name: value, limit: 10},
|
||||
function(searchResults) {
|
||||
var results = [];
|
||||
angular.forEach(searchResults, function(result) {
|
||||
if (userIds.indexOf(result.id) === -1) {
|
||||
result.name = result.full_name;
|
||||
result.type = 'user';
|
||||
results.push(result);
|
||||
}
|
||||
});
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new user or team to one of the permission levels.
|
||||
*/
|
||||
$scope.addActor = function (model) {
|
||||
if (model.type === 'user') {
|
||||
$scope.story.users.push(model);
|
||||
} else if (model.type === 'team') {
|
||||
$scope.story.teams.push(model);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a user from the permissions.
|
||||
*/
|
||||
$scope.removeUser = function (model) {
|
||||
var idx = $scope.story.users.indexOf(model);
|
||||
$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
|
||||
// ###################################################################
|
||||
|
||||
/**
|
||||
* The new task for the task form.
|
||||
*/
|
||||
$scope.newTask = new Task({
|
||||
project_id: $stateParams.project_id,
|
||||
show: true,
|
||||
status: 'todo'
|
||||
});
|
||||
|
||||
function mapTaskToProject(task) {
|
||||
Project.get({id: task.project_id}).$promise.then(function(project) {
|
||||
var idx = $scope.projectNames.indexOf(project.name);
|
||||
if (idx < 0) {
|
||||
$scope.projectNames.push(project.name);
|
||||
$scope.projects[project.name] = project;
|
||||
$scope.projects[project.name].branchNames = [];
|
||||
$scope.projects[project.name].branches = {};
|
||||
}
|
||||
Branch.get({id: task.branch_id}).$promise.then(
|
||||
function(branch) {
|
||||
var branchIdx = $scope.projects[project.name]
|
||||
.branchNames.indexOf(branch.name);
|
||||
if (branchIdx > -1) {
|
||||
$scope.projects[project.name].branches[branch.name]
|
||||
.tasks.push(task);
|
||||
} else {
|
||||
$scope.projects[project.name]
|
||||
.branches[branch.name] = branch;
|
||||
$scope.projects[project.name]
|
||||
.branches[branch.name].tasks = [task];
|
||||
$scope.projects[project.name]
|
||||
.branches[branch.name].newTask = new Task({
|
||||
story_id: $scope.story.id,
|
||||
branch_id: branch.id,
|
||||
project_id: project.id,
|
||||
status: 'todo'
|
||||
});
|
||||
$scope.projects[project.name]
|
||||
.branchNames.push(branch.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task.
|
||||
*/
|
||||
$scope.createTask = function(task, branch) {
|
||||
// Make a copy to save, so that the next task retains the
|
||||
// old information (for easier continuous editing).
|
||||
var savedTask = new Task(angular.copy(task));
|
||||
if (branch) {
|
||||
branch.tasks.push(savedTask);
|
||||
} else {
|
||||
var params = {project_id: task.project_id, name: 'master'};
|
||||
Branch.browse(params).$promise.then(function(result) {
|
||||
if (result) {
|
||||
savedTask.branch_id = result[0].id;
|
||||
mapTaskToProject(savedTask);
|
||||
}
|
||||
});
|
||||
}
|
||||
$scope.tasks.push(savedTask);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans up the project/branch/task tree.
|
||||
*
|
||||
* If there are no tasks remaining in the given branch in the given
|
||||
* project, then remove that branch from the project's list of
|
||||
* branches to display tasks for.
|
||||
*
|
||||
* If there are then no branches left in the project, remove the
|
||||
* project from the list of projects to display tasks for.
|
||||
*/
|
||||
function cleanBranchAndProject(projectName, branchName) {
|
||||
var project = $scope.projects[projectName];
|
||||
var branch = project.branches[branchName];
|
||||
var nameIdx = -1;
|
||||
|
||||
if (branch.tasks.length === 0) {
|
||||
nameIdx = project.branchNames.indexOf(branchName);
|
||||
if (nameIdx > -1) {
|
||||
project.branchNames.splice(nameIdx, 1);
|
||||
}
|
||||
delete project.branches[branchName];
|
||||
}
|
||||
if (project.branchNames.length === 0) {
|
||||
nameIdx = $scope.projectNames.indexOf(projectName);
|
||||
if (nameIdx > -1) {
|
||||
$scope.projectNames.splice(nameIdx, 1);
|
||||
}
|
||||
delete $scope.projects[projectName];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the task list.
|
||||
*/
|
||||
$scope.updateTask = function (task, fieldName, value, projectName,
|
||||
branchName) {
|
||||
var params = {id: task.id};
|
||||
params[fieldName] = value;
|
||||
task[fieldName] = value;
|
||||
if (fieldName === 'project_id') {
|
||||
var project = $scope.projects[projectName];
|
||||
var branch = project.branches[branchName];
|
||||
|
||||
var branchTaskIndex = branch.tasks.indexOf(task);
|
||||
if (branchTaskIndex > -1) {
|
||||
branch.tasks.splice(branchTaskIndex, 1);
|
||||
}
|
||||
|
||||
cleanBranchAndProject(projectName, branchName);
|
||||
mapTaskToProject(task);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.editNotes = function(task) {
|
||||
task.tempNotes = task.link;
|
||||
task.editing = true;
|
||||
};
|
||||
|
||||
$scope.cancelEditNotes = function(task) {
|
||||
task.tempNotes = '';
|
||||
task.editing = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes this task
|
||||
*/
|
||||
$scope.removeTask = function (task, projectName, branchName) {
|
||||
var taskIndex = $scope.tasks.indexOf(task);
|
||||
if (taskIndex > -1) {
|
||||
$scope.tasks.splice(taskIndex, 1);
|
||||
}
|
||||
|
||||
var project = $scope.projects[projectName];
|
||||
var branch = project.branches[branchName];
|
||||
var branchTaskIndex = branch.tasks.indexOf(task);
|
||||
if (branchTaskIndex > -1) {
|
||||
branch.tasks.splice(branchTaskIndex, 1);
|
||||
}
|
||||
cleanBranchAndProject(projectName, branchName);
|
||||
};
|
||||
|
||||
// ###################################################################
|
||||
// Tags Management
|
||||
// ###################################################################
|
||||
|
||||
/**
|
||||
* Show an input for a new tag
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
$scope.showAddTag = false;
|
||||
|
||||
$scope.toggleAddTag = function() {
|
||||
$scope.showAddTag = !$scope.showAddTag;
|
||||
};
|
||||
|
||||
$scope.addTag = function (tag_name) {
|
||||
if(!!tag_name) {
|
||||
$scope.showAddTag = false;
|
||||
$scope.tags.push(tag_name);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeTag = function (tag_name) {
|
||||
var tagIndex = $scope.tags.indexOf(tag_name);
|
||||
if (tagIndex > -1) {
|
||||
$scope.tags.splice(tagIndex, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.searchTags = function (value) {
|
||||
return Tags.browse({name: value, limit: 10}).$promise;
|
||||
};
|
||||
|
||||
$scope.updateViewValue = function (value) {
|
||||
$scope.newTag.name = value;
|
||||
};
|
||||
|
||||
$scope.newTag = {};
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
* Copyright (c) 2017 Adam Coldrick
|
||||
*
|
||||
* 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
|
||||
|
@ -21,7 +22,7 @@
|
|||
angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util',
|
||||
'ui.bootstrap'])
|
||||
.config(function ($stateProvider, $urlRouterProvider, PreferenceProvider,
|
||||
TimelineEventTypes) {
|
||||
TimelineEventTypes, SessionResolver) {
|
||||
'use strict';
|
||||
|
||||
// URL Defaults.
|
||||
|
@ -30,6 +31,9 @@ angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util',
|
|||
var queryParams = 'q&status&tags&project_group_id&'
|
||||
+ 'project_id&assignee_id';
|
||||
|
||||
var creationParams = 'title&description&project_id&'
|
||||
+ 'private&force_private&tags&team_id&user_id';
|
||||
|
||||
// Set our page routes.
|
||||
$stateProvider
|
||||
.state('sb.story', {
|
||||
|
@ -96,6 +100,15 @@ angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util',
|
|||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('sb.story.new', {
|
||||
url: '/new?' + creationParams,
|
||||
templateUrl: 'app/stories/template/new_page.html',
|
||||
controller: 'StoryNewController',
|
||||
resolve: {
|
||||
isLoggedIn: SessionResolver.requireLoggedIn,
|
||||
currentUser: SessionResolver.requireCurrentUser
|
||||
}
|
||||
});
|
||||
|
||||
// Register a preference for filtering timeline events.
|
||||
|
|
|
@ -0,0 +1,606 @@
|
|||
<!--
|
||||
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
~ Copyright (c) 2016 Codethink Ltd.
|
||||
~ Copyright (c) 2017 Adam Coldrick
|
||||
~
|
||||
~ 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="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="alert alert-danger" ng-show="story.private">
|
||||
<i class="fa fa-eye-slash"></i>
|
||||
<strong>This story is private</strong>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div ng-include
|
||||
src="'/inline/story_detail_form.html'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<hr/>
|
||||
<div ng-include src="'/inline/task_list.html'"></div>
|
||||
<hr/>
|
||||
<div ng-include src="'/inline/tags.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<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="createStory()"
|
||||
ng-disabled="!isValid()">
|
||||
Create Story
|
||||
</button>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
ng-click="preview = !preview">
|
||||
Toggle Preview
|
||||
</button>
|
||||
<a class="btn btn-link" target="_blank"
|
||||
href="http://daringfireball.net/projects/markdown/basics">
|
||||
Markdown formatting is supported.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Template for the header and description -->
|
||||
<script type="text/ng-template" id="/inline/story_detail_form.html">
|
||||
<hr ng-hide="story.private">
|
||||
<form name="storyForm" role="form" class="form-horizontal">
|
||||
<div class="form-group has-feedback"
|
||||
ng-class="{'has-error': storyForm.title.$invalid,
|
||||
'has-success': !storyForm.title.$invalid}">
|
||||
<label for="title" class="col-sm-2 control-label">
|
||||
Title
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="title"
|
||||
name="title"
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="story.title"
|
||||
required
|
||||
focus
|
||||
maxlength="100"
|
||||
placeholder="Story title"
|
||||
ng-disabled="isUpdating">
|
||||
<span class="form-control-feedback"
|
||||
ng-show="storyForm.title.$invalid">
|
||||
<i class="fa fa-times fa-lg"></i>
|
||||
</span>
|
||||
<span class="form-control-feedback"
|
||||
ng-show="!storyForm.title.$invalid">
|
||||
<i class="fa fa-check fa-lg"></i>
|
||||
</span>
|
||||
|
||||
<div class="help-block text-danger"
|
||||
ng-show="storyForm.title.$invalid">
|
||||
<span ng-show="storyForm.title.$error.required">
|
||||
A story title is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr ng-show="preview">
|
||||
<div class="form-group" ng-show="preview">
|
||||
<div class="col-sm-offset-1 col-sm-10">
|
||||
<insert-markdown content="story.description">
|
||||
</insert-markdown>
|
||||
</div>
|
||||
</div>
|
||||
<hr ng-show="preview">
|
||||
<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="story.description"
|
||||
msd-elastic
|
||||
placeholder="Enter a story description here."
|
||||
ng-disabled="isUpdating">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="!forcePrivate">
|
||||
<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="story.private"
|
||||
ng-disabled="isUpdating"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3"
|
||||
ng-show="story.private">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Teams and Users that can see this story</th>
|
||||
<th class="text-right">
|
||||
<small>
|
||||
<a href
|
||||
ng-click="showAddUser = !showAddUser">
|
||||
<i class="fa fa-plus" ng-if="!showAddUser"></i>
|
||||
<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">
|
||||
×
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="user in story.users">
|
||||
<td colspan="2">
|
||||
<i class="fa fa-sb-user"></i>
|
||||
{{user.full_name}}
|
||||
<a class="close"
|
||||
ng-click="removeUser(user)"
|
||||
ng-show="story.users.length > 1 || story.teams.length > 0">
|
||||
×
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="showAddUser">
|
||||
<td colspan="2">
|
||||
<input id="user"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Click to add a team or user"
|
||||
ng-model="asyncUser"
|
||||
typeahead-wait-ms="200"
|
||||
typeahead-editable="false"
|
||||
typeahead="actor as actor.name for actor in
|
||||
searchActors($viewValue, story.users, story.teams)"
|
||||
typeahead-loading="loadingUsers"
|
||||
typeahead-on-select="addActor($model)"
|
||||
typeahead-template-url="/inline/actor-typeahead-item.html"
|
||||
class="form-control input-sm"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Template for the Team/User typeahead items -->
|
||||
<script type="text/ng-template" id="/inline/actor-typeahead-item.html">
|
||||
<a tabindex="-1">
|
||||
<i ng-class="'fa fa-sb-' + match.model.type"></i>
|
||||
<span ng-bind-html="match.model.name | typeaheadHighlight:query"></span>
|
||||
</a>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Template for the tags list and controls -->
|
||||
<script type="text/ng-template" id="/inline/tags.html">
|
||||
<h3>Tags</h3>
|
||||
<ul class="list-inline">
|
||||
<li ng-repeat="tag in tags">
|
||||
<h4>
|
||||
<span class="label label-warning">
|
||||
{{tag}}
|
||||
<i class="fa fa-remove" ng-show="isLoggedIn"
|
||||
ng-click="removeTag(tag)"></i>
|
||||
</span>
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="input-group" ng-show="showAddTag">
|
||||
<input type="text" class="form-control" ng-model="newTag.name"
|
||||
typeahead="tag as tag.name for tag in searchTags($viewValue)"
|
||||
typeahead-on-select="updateViewValue($model.name);"/>
|
||||
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" type="button"
|
||||
ng-click="addTag(newTag.name)">Add</button>
|
||||
<button class="btn btn-default" type="button"
|
||||
ng-click="toggleAddTag()">Cancel</button>
|
||||
</span>
|
||||
</span>
|
||||
<button class="btn btn-default" ng-click="toggleAddTag()"
|
||||
ng-show="!showAddTag && isLoggedIn">
|
||||
<i class="fa fa-plus-circle"></i>
|
||||
Add
|
||||
</button>
|
||||
</script>
|
||||
|
||||
<!-- Template for the task list -->
|
||||
<script type="text/ng-template" id="/inline/task_list.html">
|
||||
<h3>Tasks</h3>
|
||||
<div ng-repeat="name in projectNames | orderBy">
|
||||
<h4>
|
||||
<i class="fa fa-sb-project text-muted"></i>
|
||||
<a href="#!/project/{{ projects[name].id }}">{{ name }}</a>
|
||||
</h4>
|
||||
<div ng-repeat="branchName in projects[name].branchNames">
|
||||
<div class="row" ng-if="branchName != 'master'">
|
||||
<div class="col-xs-3">
|
||||
<strong>
|
||||
  {{ branchName }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-if="branchName == 'master'">
|
||||
<div class="col-xs-12" ng-include src="'/inline/task_table.html'"></div>
|
||||
</div>
|
||||
<div class="row" ng-if="branchName != 'master'">
|
||||
<div class="col-xs-11 col-xs-offset-1"
|
||||
ng-include src="'/inline/task_table.html'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="newTask.show" ng-include src="'/inline/new_task.html'"></div>
|
||||
<button class="btn btn-default"
|
||||
ng-show="isLoggedIn && !newTask.show"
|
||||
ng-click="newTask.show = !newTask.show">
|
||||
<i class="fa fa-plus-circle"></i>
|
||||
Add a project
|
||||
</button>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Template for a table of tasks -->
|
||||
<script type="text/ng-template" id="/inline/task_table.html">
|
||||
<table class="table table-striped table-supercondensed">
|
||||
<tr ng-repeat="task in projects[name].branches[branchName].tasks | orderBy: '-status'"
|
||||
ng-include src="'/inline/task_list_item.html'">
|
||||
</tr>
|
||||
<tr ng-show="isLoggedIn && !projects[name].branches[branchName].showAddTask">
|
||||
<td class="text-center">
|
||||
<a href
|
||||
ng-click="projects[name].branches[branchName].showAddTask =
|
||||
!projects[name].branches[branchName].showAddTask">
|
||||
<i class="fa fa-plus-circle"></i>
|
||||
Add task affecting this project
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="projects[name].branches[branchName].showAddTask"
|
||||
ng-include src="'/inline/task_edit_form.html'">
|
||||
</tr>
|
||||
</table>
|
||||
</script>
|
||||
|
||||
<!-- Template for an item in the task list -->
|
||||
<script type="text/ng-template" id="/inline/task_list_item.html">
|
||||
<td>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 task-title">
|
||||
<div>
|
||||
<a href ng-click="showDetail = !showDetail">
|
||||
<i class="fa fa-caret-down" ng-show="showDetail"></i>
|
||||
<i class="fa fa-caret-right" ng-show="!showDetail && (task.link || isLoggedIn)"></i>
|
||||
|
||||
</a>
|
||||
<strong>{{ task.id }}</strong>
|
||||
|
||||
</div>
|
||||
<div class="title">
|
||||
<input-inline
|
||||
ng-model="task.title"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTask(task, 'title', task.title)"
|
||||
empty-prompt="Click to set title."
|
||||
empty-disabled-prompt="No title."
|
||||
maxlength="255"
|
||||
as-inline="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
<user-typeahead
|
||||
ng-model="task.assignee_id"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTask(task, 'assignee_id', task.assignee_id)"
|
||||
empty-prompt="Click to assign."
|
||||
empty-disabled-prompt="Not assigned."
|
||||
as-inline="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<task-status-dropdown
|
||||
editable="{{isLoggedIn}}"
|
||||
on-change="updateTask(task, 'status', status)"
|
||||
status="{{ task.status }}"
|
||||
/>
|
||||
</div>
|
||||
<!-- Review link should go here once implemented in the API -->
|
||||
</div>
|
||||
<div class="padded-row" ng-show="showDetail">
|
||||
<div class="col-xs-10 col-xs-offset-1">
|
||||
<project-typeahead
|
||||
auto-focus="false"
|
||||
ng-model="task.project_id"
|
||||
ng-show="changingProject"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTask(task, 'project_id', task.project_id, name, branchName);
|
||||
changingProject = false"
|
||||
empty-prompt="Click to set a project"
|
||||
empty-disabled-prompt="No project set."
|
||||
as-inline="false"
|
||||
placeholder="Enter project name">
|
||||
</project-typeahead>
|
||||
<button class="btn btn-xs btn-default"
|
||||
ng-show="isLoggedIn"
|
||||
ng-click="changingProject = !changingProject">
|
||||
{{ changingProject ? 'Cancel' : 'Change project' }}
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default"
|
||||
ng-click="removeTask(task, name, branchName)">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded-row" ng-show="showDetail">
|
||||
<div class="col-xs-10 col-xs-offset-1 task-notes"
|
||||
ng-show="isLoggedIn">
|
||||
<button class="btn btn-primary btn-xs pull-right"
|
||||
ng-show="!task.editing && task.link && isLoggedIn"
|
||||
ng-click="editNotes(task)">
|
||||
<i class="fa fa-pencil"></i> Edit
|
||||
</button>
|
||||
<insert-markdown content="task.link" ng-show="!task.editing"></insert-markdown>
|
||||
<insert-markdown content="task.tempNotes" ng-show="task.editing && notesPreview"></insert-markdown>
|
||||
<form name="taskNotesForm" ng-show="task.editing">
|
||||
<div class="form-group">
|
||||
<textarea placeholder="Enter task notes here"
|
||||
class="form-control context-edit"
|
||||
msd-elastic
|
||||
rows="3"
|
||||
ng-disabled="isUpdating"
|
||||
ng-model="task.tempNotes">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<button type="button"
|
||||
class="btn btn-default" ng-click="notesPreview = !notesPreview">
|
||||
Toggle Preview
|
||||
</button>
|
||||
<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="updateTask(task, 'link', task.tempNotes);
|
||||
cancelEditNotes(task)"
|
||||
ng-disabled="!taskNotesForm.$valid">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" ng-show="isLoggedIn"
|
||||
class="btn btn-default"
|
||||
ng-click="cancelEditNotes(task)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="padded-row pull-right" ng-show="!task.editing && !task.link">
|
||||
<button class="btn btn-primary btn-xs"
|
||||
ng-click="editNotes(task)">
|
||||
<i class="fa fa-pencil"></i> Add notes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-10 col-xs-offset-1 task-notes"
|
||||
ng-show="!isLoggedIn && task.link">
|
||||
<insert-markdown content="task.link" ng-show="!task.editing"></insert-markdown>
|
||||
<insert-markdown content="task.tempNotes" ng-show="task.editing && notesPreview"></insert-markdown>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</script>
|
||||
|
||||
<!-- Template for the task add form. -->
|
||||
<script type="text/ng-template" id="/inline/task_edit_form.html">
|
||||
<td>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 task-title">
|
||||
<div>
|
||||
<a href ng-click="showDetail = !showDetail">
|
||||
<i class="fa fa-caret-down" ng-show="showDetail"></i>
|
||||
<i class="fa fa-caret-right" ng-show="!showDetail"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<input-inline
|
||||
auto-focus="true"
|
||||
ng-model="projects[name].branches[branchName].newTask.title"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTaskInline()"
|
||||
empty-prompt="Click to assign."
|
||||
empty-disabled-prompt="Not assigned."
|
||||
maxlength="255"
|
||||
as-inline="false"
|
||||
placeholder="Enter task name"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
<user-typeahead
|
||||
auto-focus="false"
|
||||
ng-model="projects[name].branches[branchName].newTask.assignee_id"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTaskInline()"
|
||||
empty-prompt="Click to assign."
|
||||
empty-disabled-prompt="Not assigned."
|
||||
as-inline="false"
|
||||
placeholder="Assign user to task"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<task-status-dropdown
|
||||
editable="{{isLoggedIn}}"
|
||||
on-change="updateTask(projects[name].branches[branchName].newTask, 'status', status)"
|
||||
status="{{projects[name].branches[branchName].newTask.status}}"
|
||||
/>
|
||||
</div>
|
||||
<!-- Review link should go here once implemented in the API -->
|
||||
<div class="col-xs-2 text-right col-xs-offset-1">
|
||||
<button ng-click="createTask(projects[name].branches[branchName].newTask,
|
||||
projects[name].branches[branchName])"
|
||||
class="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
<button ng-click="projects[name].branches[branchName].showAddTask =
|
||||
!projects[name].branches[branchName].showAddTask"
|
||||
class="btn btn-default">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="showDetail">
|
||||
<div class="col-xs-10 col-xs-offset-1">
|
||||
<textarea ng-model="projects[name].branches[branchName].newTask.link"
|
||||
class="form-control context-edit"
|
||||
msd-elastic
|
||||
rows="3"
|
||||
ng-show="isLoggedIn"
|
||||
placeholder="Task notes">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</script>
|
||||
|
||||
<!-- Template for creating a task in an as-yet unrelated project -->
|
||||
<script type="text/ng-template" id="/inline/new_task.html">
|
||||
<div>
|
||||
<h4>
|
||||
<i class="fa fa-sb-project text-muted"></i>
|
||||
<project-typeahead
|
||||
auto-focus="false"
|
||||
ng-model="newTask.project_id"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTaskInline()"
|
||||
empty-prompt="Click to set a project"
|
||||
empty-disabled-prompt="No project set."
|
||||
as-inline="false"
|
||||
placeholder="Enter project name">
|
||||
</project-typeahead>
|
||||
</h4>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-supercondensed">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 task-title">
|
||||
<div>
|
||||
<a href ng-click="showDetail = !showDetail">
|
||||
<i class="fa fa-caret-down" ng-show="showDetail"></i>
|
||||
<i class="fa fa-caret-right" ng-show="!showDetail"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<input-inline
|
||||
auto-focus="true"
|
||||
ng-model="newTask.title"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTaskInline()"
|
||||
empty-prompt="Click to assign."
|
||||
empty-disabled-prompt="Not assigned."
|
||||
maxlength="255"
|
||||
as-inline="false"
|
||||
placeholder="Enter task name"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
<user-typeahead
|
||||
auto-focus="false"
|
||||
ng-model="newTask.assignee_id"
|
||||
enabled="isLoggedIn"
|
||||
on-change="updateTaskInline()"
|
||||
empty-prompt="Click to assign."
|
||||
empty-disabled-prompt="Not assigned."
|
||||
as-inline="false"
|
||||
placeholder="Assign user to task"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<task-status-dropdown
|
||||
editable="{{isLoggedIn}}"
|
||||
on-change="updateTask(newTask, 'status', status)"
|
||||
status="{{newTask.status}}"
|
||||
/>
|
||||
</div>
|
||||
<!-- Review link should go here once implemented in the API -->
|
||||
<div class="col-xs-2 text-right col-xs-offset-1">
|
||||
<button ng-click="createTask(newTask)"
|
||||
class="btn btn-primary">
|
||||
Add
|
||||
</button>
|
||||
<button ng-click="newTask.show = !newTask.show"
|
||||
ng-if="tasks.length > 0"
|
||||
class="btn btn-default">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="showDetail">
|
||||
<div class="col-xs-10 col-xs-offset-1">
|
||||
<textarea ng-model="newTask.link"
|
||||
class="form-control context-edit"
|
||||
msd-elastic
|
||||
rows="3"
|
||||
ng-show="isLoggedIn"
|
||||
placeholder="Task notes">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
Loading…
Reference in New Issue