Comments UI
- Added comments resource - Added comment list controller to resolve author names. - Updated detail controller to allow comment control. - Major rework of the story UI to make room for a tasks and discussions. - Refactor of story controllers to separate concernse into tasks, comments, story. - Collapse of the story statechart. - New LESS file for discussions. - New directive: ngShiftEnter, to allow shift-enter comment submission. - New directive: contenteditable, to allow in-context editing. Change-Id: I8be85da39097683eb7660835c1cbc40524dd5b2c
This commit is contained in:
parent
5751ac7eb9
commit
f0232defc3
33
src/app/services/resource/comment.js
Normal file
33
src/app/services/resource/comment.js
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 discussions that
|
||||
* are surrounding stories.
|
||||
*
|
||||
* @see storyboardApiSignature
|
||||
*/
|
||||
angular.module('sb.services').factory('Comment',
|
||||
function ($resource, storyboardApiBase, storyboardApiSignature) {
|
||||
'use strict';
|
||||
|
||||
return $resource(storyboardApiBase + '/stories/:story_id/comments/:id',
|
||||
{
|
||||
id: '@id',
|
||||
story_id: '@story_id'
|
||||
},
|
||||
storyboardApiSignature);
|
||||
});
|
42
src/app/stories/controllers/story_delete_controller.js
Normal file
42
src/app/stories/controllers/story_delete_controller.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Story detail & manipulation controller.
|
||||
*/
|
||||
angular.module('sb.story').controller('StoryDeleteController',
|
||||
function ($log, $scope, $state, story, $modalInstance) {
|
||||
'use strict';
|
||||
|
||||
$scope.story = story;
|
||||
|
||||
// Set our progress flags and clear previous error conditions.
|
||||
$scope.isUpdating = true;
|
||||
$scope.error = {};
|
||||
|
||||
$scope.remove = function () {
|
||||
$scope.story.$delete(
|
||||
function () {
|
||||
$modalInstance.dismiss('success');
|
||||
$state.go('project.list');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$modalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
@ -1,14 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* 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
|
||||
* 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.
|
||||
@ -18,7 +18,7 @@
|
||||
* Story detail & manipulation controller.
|
||||
*/
|
||||
angular.module('sb.story').controller('StoryDetailController',
|
||||
function ($scope, $state, $stateParams, Story, Task, Project) {
|
||||
function ($log, $scope, $state, $stateParams, $modal, Story) {
|
||||
'use strict';
|
||||
|
||||
// Parse the ID
|
||||
@ -26,15 +26,33 @@ angular.module('sb.story').controller('StoryDetailController',
|
||||
parseInt($stateParams.storyId, 10) :
|
||||
null;
|
||||
|
||||
/**
|
||||
* Generic service error handler. Assigns errors to the view's scope,
|
||||
* and unsets our flags.
|
||||
*/
|
||||
function handleServiceError(error) {
|
||||
// We've encountered an error.
|
||||
$scope.error = error;
|
||||
$scope.isLoading = false;
|
||||
$scope.isUpdating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets our loading flags.
|
||||
*/
|
||||
function handleServiceSuccess() {
|
||||
$scope.isLoading = false;
|
||||
$scope.isUpdating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The story we're manipulating right now.
|
||||
*/
|
||||
$scope.story = {};
|
||||
$scope.tasks = [];
|
||||
$scope.newTask = new Task({
|
||||
story_id: id
|
||||
});
|
||||
$scope.projects = Project.query({});
|
||||
$scope.story = Story.get(
|
||||
{'id': id},
|
||||
handleServiceSuccess,
|
||||
handleServiceError
|
||||
);
|
||||
|
||||
/**
|
||||
* UI flag for when we're initially loading the view.
|
||||
@ -58,71 +76,7 @@ angular.module('sb.story').controller('StoryDetailController',
|
||||
$scope.error = {};
|
||||
|
||||
/**
|
||||
* Generic service error handler. Assigns errors to the view's scope,
|
||||
* and unsets our flags.
|
||||
*/
|
||||
function handleServiceError(error) {
|
||||
// We've encountered an error.
|
||||
$scope.error = error;
|
||||
$scope.isLoading = false;
|
||||
$scope.isUpdating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the tasks for this story
|
||||
*/
|
||||
function loadTasks() {
|
||||
$scope.tasks = [];
|
||||
|
||||
Task.query(
|
||||
{story_id: id},
|
||||
function (result) {
|
||||
$scope.tasks = result;
|
||||
},
|
||||
handleServiceError
|
||||
);
|
||||
}
|
||||
|
||||
// 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.'
|
||||
};
|
||||
$scope.isLoading = false;
|
||||
} else {
|
||||
// We've got an ID, so let's load it...
|
||||
Story.read(
|
||||
{'id': id},
|
||||
function (result) {
|
||||
// We've got a result, assign it to the view and unset our
|
||||
// loading flag.
|
||||
$scope.story = result;
|
||||
$scope.newTask.project_id = result.project_id;
|
||||
$scope.isLoading = false;
|
||||
},
|
||||
handleServiceError
|
||||
);
|
||||
|
||||
loadTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task.
|
||||
*/
|
||||
$scope.addTask = function () {
|
||||
$scope.newTask.$save(function () {
|
||||
loadTasks();
|
||||
$scope.newTask = new Task();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Scope method, invoke this when you want to update the project.
|
||||
* Scope method, invoke this when you want to update the story.
|
||||
*/
|
||||
$scope.update = function () {
|
||||
// Set our progress flags and clear previous error conditions.
|
||||
@ -130,29 +84,25 @@ angular.module('sb.story').controller('StoryDetailController',
|
||||
$scope.error = {};
|
||||
|
||||
// Invoke the save method and wait for results.
|
||||
$scope.story.$update(
|
||||
function (result) {
|
||||
// Unset our loading flag and navigate to the detail view.
|
||||
$scope.isUpdating = false;
|
||||
$state.go('story.detail.overview', {storyId: result.id});
|
||||
},
|
||||
handleServiceError
|
||||
);
|
||||
$scope.story.$update(handleServiceSuccess, handleServiceError);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete method.
|
||||
*/
|
||||
$scope.remove = function () {
|
||||
// Set our progress flags and clear previous error conditions.
|
||||
$scope.isUpdating = true;
|
||||
$scope.error = {};
|
||||
|
||||
$scope.story.$delete(
|
||||
function () {
|
||||
$state.go('project.list');
|
||||
},
|
||||
handleServiceError
|
||||
);
|
||||
var modalInstance = $modal.open({
|
||||
templateUrl: 'app/templates/story/delete.html',
|
||||
controller: 'StoryDeleteController',
|
||||
resolve: {
|
||||
story: function () {
|
||||
return $scope.story;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return the modal's promise.
|
||||
return modalInstance.result;
|
||||
};
|
||||
});
|
||||
|
88
src/app/stories/controllers/story_discussion_controller.js
Normal file
88
src/app/stories/controllers/story_discussion_controller.js
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 used for the comments section on the story detail page.
|
||||
*/
|
||||
angular.module('sb.story').controller('StoryDiscussionController',
|
||||
function ($log, $scope, $state, $stateParams, Project, Comment) {
|
||||
'use strict';
|
||||
|
||||
// Parse the ID
|
||||
var id = $stateParams.hasOwnProperty('storyId') ?
|
||||
parseInt($stateParams.storyId, 10) :
|
||||
null;
|
||||
|
||||
/**
|
||||
* The story we're manipulating right now.
|
||||
*/
|
||||
$scope.comments = Comment.query({story_id: id});
|
||||
|
||||
/**
|
||||
* The new comment backing the input form.
|
||||
*/
|
||||
$scope.newComment = new Comment({story_id: id});
|
||||
|
||||
/**
|
||||
* UI flag for when we're initially loading the view.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
$scope.isLoading = true;
|
||||
|
||||
/**
|
||||
* UI view for when we're trying to save a comment.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
$scope.isSavingComment = false;
|
||||
|
||||
/**
|
||||
* Any error objects returned from the services.
|
||||
*
|
||||
* @type {{}}
|
||||
*/
|
||||
$scope.error = {};
|
||||
|
||||
/**
|
||||
* Add a comment
|
||||
*/
|
||||
$scope.addComment = function () {
|
||||
|
||||
function resetSavingFlag() {
|
||||
$scope.isSavingComment = false;
|
||||
}
|
||||
|
||||
// Do nothing if the comment is empty
|
||||
if (!$scope.newComment.content) {
|
||||
$log.warn('No content in comment, discarding submission');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.isSavingComment = true;
|
||||
|
||||
// Author ID will be automatically attached by the service, so
|
||||
// don't inject it into the conversation until it comes back.
|
||||
$scope.newComment.$create(
|
||||
function (comment) {
|
||||
$scope.comments.push(comment);
|
||||
$scope.newComment = new Comment({story_id: id});
|
||||
resetSavingFlag();
|
||||
},
|
||||
resetSavingFlag
|
||||
);
|
||||
};
|
||||
});
|
@ -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 used for the comments section on the story detail page.
|
||||
*/
|
||||
angular.module('sb.story').controller('StoryDiscussionItemController',
|
||||
function ($scope, User) {
|
||||
'use strict';
|
||||
|
||||
$scope.author = User.get({
|
||||
id: $scope.comment.author_id
|
||||
});
|
||||
});
|
98
src/app/stories/controllers/story_task_list_controller.js
Normal file
98
src/app/stories/controllers/story_task_list_controller.js
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 that provides methods that allow editing of a story.
|
||||
*/
|
||||
angular.module('sb.story').controller('StoryTaskListController',
|
||||
function ($log, $scope, $state, $stateParams, Task, Project) {
|
||||
'use strict';
|
||||
|
||||
// Parse the ID
|
||||
var id = $stateParams.hasOwnProperty('storyId') ?
|
||||
parseInt($stateParams.storyId, 10) :
|
||||
null;
|
||||
|
||||
/**
|
||||
* The current list of tasks
|
||||
*/
|
||||
$scope.tasks = [];
|
||||
|
||||
/**
|
||||
* Display toggle for the add task form.
|
||||
*
|
||||
* @TODO(krotscheck) Remove, we're not using this interface pattern
|
||||
* anywhere else. Should probably be a modal...?
|
||||
*/
|
||||
$scope.showAddTaskForm = false;
|
||||
|
||||
/**
|
||||
* The new task for the task form.
|
||||
*/
|
||||
$scope.newTask = new Task({ story_id: id });
|
||||
|
||||
/**
|
||||
* Projects for the new task form
|
||||
*/
|
||||
$scope.projects = Project.query();
|
||||
|
||||
/**
|
||||
* UI flag for when we're initially loading the view.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
$scope.isLoading = true;
|
||||
|
||||
/**
|
||||
* Any error objects returned from the services.
|
||||
*
|
||||
* @type {{}}
|
||||
*/
|
||||
$scope.error = {};
|
||||
|
||||
/**
|
||||
* Loads the tasks for this story
|
||||
*/
|
||||
function loadTasks() {
|
||||
$scope.tasks = [];
|
||||
|
||||
Task.query(
|
||||
{story_id: id},
|
||||
function (result) {
|
||||
$scope.tasks = result;
|
||||
},
|
||||
function (error) {
|
||||
// We've encountered an error.
|
||||
$scope.error = error;
|
||||
$scope.isLoading = false;
|
||||
$scope.isUpdating = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task.
|
||||
*/
|
||||
$scope.addTask = function () {
|
||||
$scope.newTask.$save(function () {
|
||||
loadTasks();
|
||||
$scope.newTask = new Task({story_id: id});
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize our view
|
||||
loadTasks();
|
||||
});
|
@ -18,25 +18,12 @@
|
||||
* 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', 'sb.auth'])
|
||||
.config(function ($stateProvider, $urlRouterProvider, SessionResolver) {
|
||||
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
|
||||
@ -52,26 +39,6 @@ angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util', 'sb.auth'])
|
||||
})
|
||||
.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',
|
||||
resolve: {
|
||||
isLoggedIn: SessionResolver.requireLoggedIn
|
||||
}
|
||||
})
|
||||
.state('story.detail.delete', {
|
||||
url: '/delete',
|
||||
templateUrl: 'app/templates/story/delete.html',
|
||||
resolve: {
|
||||
isLoggedIn: SessionResolver.requireLoggedIn
|
||||
}
|
||||
templateUrl: 'app/templates/story/detail.html'
|
||||
});
|
||||
});
|
||||
|
@ -13,22 +13,29 @@
|
||||
~ 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()">×</button>
|
||||
<h3 class="panel-title">{{story.title}}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<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 story?
|
||||
</h2>
|
||||
|
||||
<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 story?
|
||||
</h2>
|
||||
<p class="text-center lead">
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<p class="text-center lead">
|
||||
This will set the story to a "deleted" state, and any
|
||||
tasks will no longer be visible.
|
||||
</p>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="" class="btn btn-danger" ng-click="remove()">
|
||||
Remove this story
|
||||
</a>
|
||||
<div class="text-center">
|
||||
<a href="" class="btn btn-danger" ng-click="remove()">
|
||||
Remove this story
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -13,29 +13,181 @@
|
||||
~ License for the specific language governing permissions and limitations
|
||||
~ under the License.
|
||||
-->
|
||||
<div class="container" ng-controller="StoryDetailController">
|
||||
|
||||
<div class="container">
|
||||
<h1 class="no-border no-margin-bottom">Story detail: {{story.title}}</h1>
|
||||
<ul class="nav nav-tabs nav-tabs-down nav-thick">
|
||||
<li active-path="^\/story\/[0-9]+\/overview.*">
|
||||
<a href="#!/story/{{story.id}}/overview">
|
||||
Overview
|
||||
</a>
|
||||
</li>
|
||||
<li active-path="^\/story\/[0-9]+\/edit.*"
|
||||
ng-show="isLoggedIn">
|
||||
<a href="#!/story/{{story.id}}/edit">
|
||||
Edit
|
||||
</a>
|
||||
</li>
|
||||
<li active-path="^\/story\/[0-9]+\/delete.*"
|
||||
ng-show="isLoggedIn">
|
||||
<a href="#!/story/{{story.id}}/delete">
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<!-- Begin header -->
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<h1 contenteditable="{{isLoggedIn}}"
|
||||
ng-model="story.title"></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ui-view></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<!-- Begin Description -->
|
||||
<p contenteditable="{{isLoggedIn}}"
|
||||
ng-model="story.description">
|
||||
</p>
|
||||
|
||||
<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="story.description.length < 1 ||
|
||||
story.title.length < 1"
|
||||
ng-show="isLoggedIn">
|
||||
Save
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-default"
|
||||
ng-click="remove()"
|
||||
ng-show="isLoggedIn">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
<!-- Begin Discussion -->
|
||||
<div ng-controller="StoryDiscussionController">
|
||||
<h4>Discussion</h4>
|
||||
|
||||
<div class="discussion">
|
||||
<div class="alert alert-warning"
|
||||
ng-show="comments.length == 0">
|
||||
The discussion hasn't started yet
|
||||
</div>
|
||||
<div ng-repeat="comment in comments"
|
||||
ng-controller="StoryDiscussionItemController"
|
||||
class="discussion-comment">
|
||||
<p class="discussion-comment-author">
|
||||
{{author.full_name}}
|
||||
</p>
|
||||
|
||||
<p ng-show="comment.content">{{comment.content}}</p>
|
||||
|
||||
<p><em ng-hide="comment.content"
|
||||
class="text-muted">
|
||||
The author left a blank comment.
|
||||
</em></p>
|
||||
</div>
|
||||
|
||||
<form class="discussion-comment-form comment"
|
||||
id="commentForm"
|
||||
name="commentForm"
|
||||
ng-show="isLoggedIn">
|
||||
<div class="form-group">
|
||||
<textarea id="comment"
|
||||
placeholder="Enter your comment here"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
required
|
||||
ng-disabled="isSavingComment"
|
||||
ng-shift-enter="addComment()"
|
||||
ng-model="newComment.content">
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<button type="button"
|
||||
class="btn btn-primary pull-right"
|
||||
ng-click="addComment()"
|
||||
ng-disabled="!commentForm.$valid || isSavingComment">
|
||||
Comment
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="hidden-md hidden-lg"/>
|
||||
</div>
|
||||
|
||||
<!-- Begin Task List -->
|
||||
<div class="col-xs-12 col-md-4" ng-controller="StoryTaskListController">
|
||||
<button type="button"
|
||||
ng-click="showAddTaskForm = !showAddTaskForm"
|
||||
ng-show="isLoggedIn"
|
||||
class="pull-right btn btn-default btn-sm">
|
||||
<i class="fa fa-plus"
|
||||
ng-hide="showAddTaskForm"></i>
|
||||
<i class="fa fa-minus"
|
||||
ng-show="showAddTaskForm"></i>
|
||||
</button>
|
||||
|
||||
<h4>Tasks</h4>
|
||||
|
||||
<div class="well" ng-show="showAddTaskForm">
|
||||
<form role="form" name="taskForm">
|
||||
<div class="form-group row">
|
||||
<label for="title" class="col-sm-2 control-label">
|
||||
Task Title:
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<input id="title"
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="newTask.title"
|
||||
required
|
||||
placeholder="Task Title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="project" class="col-sm-2 control-label">
|
||||
Task Project:
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<select ng-model="newTask.project_id"
|
||||
id="project"
|
||||
name="project"
|
||||
class="form-control"
|
||||
required
|
||||
ng-options="p.id as p.name for p in projects"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-default"
|
||||
ng-click="addTask()"
|
||||
ng-disabled="!taskForm.$valid">
|
||||
Add task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
<tr ng-repeat="task in tasks"
|
||||
ng-controller="StoryTaskListItemController">
|
||||
<td>
|
||||
<span class="label label-default pull-right">
|
||||
{{task.status}}
|
||||
</span>
|
||||
|
||||
<p><strong>
|
||||
<a href="">{{task.title}}</a>
|
||||
</strong></p>
|
||||
<a href="#!/project/{{project.id}}/overview"
|
||||
ng-show="project">
|
||||
{{project.name}}
|
||||
</a>
|
||||
|
||||
<span class="text-danger" ng-hide="project">
|
||||
Project not found
|
||||
</span>
|
||||
<small class="text-muted">{{task.description}}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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="storyForm">
|
||||
<div class="form-group">
|
||||
<label for="title" class="col-sm-2 control-label">
|
||||
Title:
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<input id="title"
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="story.title"
|
||||
required
|
||||
placeholder="Story Title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description"
|
||||
class="col-sm-2 control-label">
|
||||
Story Description
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<textarea id="description"
|
||||
class="form-control"
|
||||
ng-model="story.description"
|
||||
required
|
||||
placeholder="A brief story 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="!storyForm.$valid">
|
||||
Save Changes
|
||||
</button>
|
||||
<a href="#!/story/list"
|
||||
class="btn btn-default">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,144 +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-9 col-xs-11">
|
||||
<p ng-show="story.description">
|
||||
{{story.description}}
|
||||
</p>
|
||||
<em ng-hide="story.description"
|
||||
class="text-muted">
|
||||
No description available
|
||||
</em>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-1">
|
||||
<a href=""
|
||||
ng-click="showAddTaskForm = !showAddTaskForm"
|
||||
ng-show="isLoggedIn"
|
||||
class="pull-right btn btn-default btn-sm">
|
||||
<span ng-hide="showAddTaskForm">
|
||||
<i class="fa fa-plus"></i>
|
||||
<span class="hidden-xs">Add Task</span>
|
||||
</span>
|
||||
<span ng-show="showAddTaskForm">
|
||||
<i class="fa fa-minus"></i>
|
||||
<span class="hidden-xs">Add Task</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="showAddTaskForm">
|
||||
<br/>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<div class="well">
|
||||
<form role="form" name="taskForm">
|
||||
<div class="form-group row">
|
||||
<label for="title" class="col-sm-2 control-label">
|
||||
Task Title:
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<input id="title"
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="newTask.title"
|
||||
required
|
||||
placeholder="Task Title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="project" class="col-sm-2 control-label">
|
||||
Task Project:
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<select ng-model="newTask.project_id"
|
||||
id="project"
|
||||
name="project"
|
||||
class="form-control"
|
||||
required
|
||||
ng-options="p.id as p.name for p in projects"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-default"
|
||||
ng-click="addTask()"
|
||||
ng-disabled="!taskForm.$valid">
|
||||
Add task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-sm-1">
|
||||
<small>ID</small>
|
||||
</th>
|
||||
<th class="col-sm-6">
|
||||
<small>Title</small>
|
||||
</th>
|
||||
<th class="col-sm-3">
|
||||
<small>Project</small>
|
||||
</th>
|
||||
<th class="col-sm-2">
|
||||
<small>Status</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="task in tasks"
|
||||
ng-controller="StoryTaskListItemController">
|
||||
<td>
|
||||
<p><strong>
|
||||
<a href="">{{task.id}}</a>
|
||||
</strong></p>
|
||||
</td>
|
||||
<td>
|
||||
<p><strong>
|
||||
<a href="">{{task.title}}</a>
|
||||
</strong></p>
|
||||
<small class="text-muted">{{task.description}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
<a href="#!/project/{{project.id}}/overview"
|
||||
ng-show="project">
|
||||
{{project.name}}
|
||||
</a>
|
||||
|
||||
<span class="text-danger" ng-hide="project">
|
||||
Project not found
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-success">{{task.status}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
43
src/app/util/directive/contenteditable.js
Normal file
43
src/app/util/directive/contenteditable.js
Normal file
@ -0,0 +1,43 @@
|
||||
angular.module('sb.util').
|
||||
directive('contenteditable', function () {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function (scope, element, attrs, ngModel) {
|
||||
// Write data to the model
|
||||
function read() {
|
||||
var e = element[0];
|
||||
var html = e.innerText || '';
|
||||
ngModel.$setViewValue(html);
|
||||
|
||||
// If you copy/paste between contenteditable fields, it
|
||||
// drags the HTML tags along. Unfortunately not all
|
||||
// browsers will let us modify the clipboard in flight,
|
||||
// so we have to selectively rewrite it here. This also
|
||||
// strips all HTML tags out - which is good for sanitizing,
|
||||
// but we'll want to add a rich text editor.
|
||||
if (e.innerHTML !== e.innerText) {
|
||||
e.innerHTML = e.innerText;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ngModel) {
|
||||
return; // do nothing if no ng-model
|
||||
}
|
||||
|
||||
// Specify how UI should be updated
|
||||
ngModel.$render = function () {
|
||||
element.html(ngModel.$viewValue || '');
|
||||
};
|
||||
|
||||
// Listen for change events to enable binding
|
||||
element.on('blur keyup change', function () {
|
||||
scope.$apply(read);
|
||||
});
|
||||
read(); // initialize
|
||||
|
||||
}
|
||||
};
|
||||
});
|
38
src/app/util/directive/shift_enter.js
Normal file
38
src/app/util/directive/shift_enter.js
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2013 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This directive adds the ng-shift-enter directive. It intercepts keystrokes
|
||||
* and will execute the bound method if that keystroke is the enter key.
|
||||
*
|
||||
* @author Michael Krotscheck
|
||||
*/
|
||||
angular.module('sb.util').directive('ngShiftEnter', function () {
|
||||
'use strict';
|
||||
|
||||
return function (scope, element, attrs) {
|
||||
|
||||
element.bind('keydown keypress', function (event) {
|
||||
if (event.which === 13 && event.shiftKey) {
|
||||
scope.$apply(function () {
|
||||
scope.$eval(attrs.ngShiftEnter);
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
24
src/styles/bootstrap_addons.less
vendored
24
src/styles/bootstrap_addons.less
vendored
@ -17,7 +17,7 @@
|
||||
/**
|
||||
* Generic overrides and addons for bootstrap.
|
||||
*/
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1, h2, h3, h4, h5, h6, p {
|
||||
|
||||
&.no-margin {
|
||||
margin: 0px;
|
||||
@ -157,4 +157,26 @@ table.table.table-outlined {
|
||||
// to get rid of.
|
||||
.modal-content .panel {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
// Added highlighting for enabled conteneditable areas
|
||||
*[contenteditable=true] {
|
||||
|
||||
// Most of this copied from .form-control, removing the font declarations.
|
||||
|
||||
padding: @padding-base-vertical @padding-base-horizontal;
|
||||
color: @input-color;
|
||||
background-color: @input-bg;
|
||||
background-image: none;
|
||||
border: 1px solid @white;
|
||||
border-radius: @input-border-radius;
|
||||
|
||||
&:hover {
|
||||
border-color: @input-border;
|
||||
.box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
|
||||
}
|
||||
.transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s");
|
||||
|
||||
// Customize the `:focus` state to imitate native WebKit styles.
|
||||
.form-control-focus();
|
||||
}
|
49
src/styles/discussion.less
Normal file
49
src/styles/discussion.less
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Discussion styles.
|
||||
*/
|
||||
.discussion {
|
||||
.discussion-comment-none {
|
||||
border-top: 1px solid @table-border-color;
|
||||
border-bottom: 1px solid @table-border-color;
|
||||
padding: @table-cell-padding;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.discussion-comment {
|
||||
* {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.discussion-comment-author {
|
||||
border-top: 1px solid @table-border-color;
|
||||
background-color: @gray-lighter;
|
||||
padding: @table-condensed-cell-padding;
|
||||
}
|
||||
|
||||
> p {
|
||||
padding: @table-cell-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-comment-form {
|
||||
&:last-child {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
@ -31,4 +31,5 @@
|
||||
@import './custom_font_icons.less';
|
||||
// Module specific styles
|
||||
@import './body.less';
|
||||
@import './auth.less';
|
||||
@import './auth.less';
|
||||
@import './discussion.less';
|
Loading…
x
Reference in New Issue
Block a user