Merge branch 'dev' into start-splitting-up-work-package-details-controller

Conflicts:
	app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
pull/1630/head
Till Breuer 10 years ago
commit 56e4b7a49c
  1. 56
      app/assets/javascripts/angular/directives/components/activity-comment-directive.js
  2. 2
      app/assets/javascripts/angular/directives/components/authoring-directive.js
  3. 35
      app/assets/javascripts/angular/filters/latest-items-filter.js
  4. 14
      app/assets/javascripts/angular/helpers/components/path-helper.js
  5. 74
      app/assets/javascripts/angular/services/activity-service.js
  6. 1
      app/assets/javascripts/angular/services/timezone-service.js
  7. 6
      app/assets/javascripts/angular/work_packages/controllers/details-tab-watchers-controller.js
  8. 76
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  9. 43
      app/assets/javascripts/angular/work_packages/tabs/editable-comment-directive.js
  10. 50
      app/assets/javascripts/angular/work_packages/tabs/exclusive-edit-directive.js
  11. 51
      app/assets/javascripts/angular/work_packages/tabs/user-activity-directive.js
  12. 4
      app/assets/stylesheets/_work_packages.scss
  13. 41
      app/assets/stylesheets/content/_work_packages_details_activities.sass
  14. 1
      app/assets/stylesheets/default.css.sass
  15. 3
      app/assets/stylesheets/layout/_split_view.sass
  16. 4
      bower.json
  17. 2
      config/locales/js-de.yml
  18. 2
      config/locales/js-en.yml
  19. 3
      karma.conf.js
  20. 55
      karma/tests/filters/latest-items-filter-test.js
  21. 82
      karma/tests/services/activity-service-test.js
  22. 29
      karma/tests/services/timezone-service-test.js
  23. 25
      lib/api/v3/activities/activities_api.rb
  24. 7
      lib/api/v3/activities/activity_representer.rb
  25. 31
      lib/api/v3/work_packages/work_packages_api.rb
  26. 7
      public/templates/components/activity_comment.html
  27. 5
      public/templates/work_packages/tabs/_editable_comment.html
  28. 27
      public/templates/work_packages/tabs/_user_activity.html
  29. 35
      public/templates/work_packages/tabs/activity.html
  30. 25
      public/templates/work_packages/tabs/overview.html
  31. 82
      spec/requests/activities_api_spec.rb
  32. 53
      spec/requests/api_helper_spec.rb
  33. 61
      spec/requests/work_packages_api_spec.rb
  34. 2921
      vendor/assets/javascripts/moment-timezone/moment-timezone-data.js

@ -0,0 +1,56 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
// TODO move to UI components
angular.module('openproject.uiComponents')
.directive('activityComment', ['I18n', 'ActivityService', 'ConfigurationService', function(I18n, ActivityService, ConfigurationService) {
return {
restrict: 'E',
replace: true,
scope: {
workPackage: '=',
activities: '='
},
templateUrl: '/templates/components/activity_comment.html',
link: function(scope, element, attrs) {
scope.title = I18n.t('js.label_add_comment_title');
scope.buttonTitle = I18n.t('js.label_add_comment');
scope.createComment = function() {
var comment = angular.element('#add-comment-text').val();
var descending = ConfigurationService.commentsSortedInDescendingOrder();
ActivityService.createComment(scope.workPackage.props.id, scope.activities, descending, comment)
.then(function(response){
angular.element('#add-comment-text').val('');
return response;
});
}
}
};
}]);

@ -46,7 +46,7 @@ angular.module('openproject.uiComponents')
scope.authorLink = '<a href="'+ PathHelper.userPath(scope.author.id) + '">' + scope.author.name + '</a>'; scope.authorLink = '<a href="'+ PathHelper.userPath(scope.author.id) + '">' + scope.author.name + '</a>';
if (scope.activity) { if (scope.activity) {
scope.timestamp = '<a title="' + time + '" href="' + PathHelper.activityPath(scope.project, createdOn.format('YYYY-MM-DD')) + '">' + timeago + '</a>'; scope.timestamp = '<a title="' + time + '" href="' + PathHelper.activityFromPath(scope.project, createdOn.format('YYYY-MM-DD')) + '">' + timeago + '</a>';
} else { } else {
scope.timestamp = '<span class="timestamp" title="' + time + '">' + timeago + '</span>'; scope.timestamp = '<span class="timestamp" title="' + time + '">' + timeago + '</span>';
} }

@ -0,0 +1,35 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.uiComponents')
.filter('latestItems', function(){
return function(items, isDescending, visible){
return isDescending ? items.slice(0, visible) : items.slice(-visible).reverse();
}
});

@ -36,7 +36,19 @@ angular.module('openproject.helpers')
apiV3: '/api/v3', apiV3: '/api/v3',
staticBase: window.appBasePath ? window.appBasePath : '', staticBase: window.appBasePath ? window.appBasePath : '',
activityPath: function(projectIdentifier, from) { activityPath: function(activityId) {
return 'activities/' + activityId;
},
activitiesPath: function(workPackageId) {
var path = '';
if(workPackageId) {
path = '/work_packages/' + workPackageId;
}
path = path + '/activities';
return path;
},
activityFromPath: function(projectIdentifier, from) {
var link = '/activity'; var link = '/activity';
if (projectIdentifier) { if (projectIdentifier) {

@ -0,0 +1,74 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.services')
.service('ActivityService', ['HALAPIResource',
'$http',
'PathHelper', function(HALAPIResource, $http, PathHelper){
var ActivityService = {
createComment: function(workPackageId, activities, descending, comment) {
var resource = HALAPIResource.setup(PathHelper.activitiesPath(workPackageId));
var options = {
ajax: {
method: "POST",
data: { comment: comment }
}
};
return resource.fetch(options).then(function(activity){
// We are unable to add to the work package's embedded activities directly
if(activity) {
if(descending){
activities.unshift(activity);
} else {
activities.push(activity);
}
return activity;
}
});
},
updateComment: function(activityId, comment) {
var resource = HALAPIResource.setup(PathHelper.activityPath(activityId));
var options = {
ajax: {
method: "PUT",
data: { comment: comment }
}
};
return resource.fetch(options).then(function(activity){
return activity;
});
}
}
return ActivityService;
}]);

@ -34,6 +34,7 @@ angular.module('openproject.services')
var d = moment.utc(date); var d = moment.utc(date);
if (ConfigurationService.isTimezoneSet()) { if (ConfigurationService.isTimezoneSet()) {
d.local();
d.tz(ConfigurationService.timezone()); d.tz(ConfigurationService.timezone());
} }

@ -83,4 +83,10 @@ angular.module('openproject.workPackages.controllers')
.fetch({ajax: {method: 'POST'}}) .fetch({ajax: {method: 'POST'}})
.then($scope.refreshWorkPackage, $scope.outputError); .then($scope.refreshWorkPackage, $scope.outputError);
}; };
$scope.deleteWatcher = function(watcher) {
watcher.links.removeWatcher
.fetch({ ajax: watcher.links.removeWatcher.props })
.then($scope.refreshWorkPackage, $scope.outputError);
};
}]); }]);

@ -28,20 +28,28 @@
angular.module('openproject.workPackages.controllers') angular.module('openproject.workPackages.controllers')
.constant('VISIBLE_LATEST')
.controller('WorkPackageDetailsController', [ .controller('WorkPackageDetailsController', [
'$scope', '$scope',
'latestTab', 'latestTab',
'workPackage', 'workPackage',
'I18n', 'I18n',
'VISIBLE_LATEST',
'$q', '$q',
'ConfigurationService', 'ConfigurationService',
function($scope, latestTab, workPackage, I18n,$q, ConfigurationService) { function($scope, latestTab, workPackage, I18n, VISIBLE_LATEST, $q, ConfigurationService) {
$scope.$on('$stateChangeSuccess', function(event, toState){ $scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name); latestTab.registerState(toState.name);
}); });
$scope.$on('workPackageRefreshRequired', function(event, toState){
refreshWorkPackage();
});
// initialization // initialization
setWorkPackage(workPackage); setWorkPackageScopeProperties(workPackage);
$scope.I18n = I18n; $scope.I18n = I18n;
$scope.$parent.preselectedWorkPackageId = $scope.workPackage.props.id; $scope.$parent.preselectedWorkPackageId = $scope.workPackage.props.id;
$scope.maxDescriptionLength = 800; $scope.maxDescriptionLength = 800;
@ -49,17 +57,10 @@ angular.module('openproject.workPackages.controllers')
function refreshWorkPackage() { function refreshWorkPackage() {
workPackage.links.self workPackage.links.self
.fetch({force: true}) .fetch({force: true})
.then(setWorkPackage); .then(setWorkPackageScopeProperties);
} }
$scope.refreshWorkPackage = refreshWorkPackage; // expose to child controllers $scope.refreshWorkPackage = refreshWorkPackage; // expose to child controllers
function setWorkPackage(workPackage) {
$scope.workPackage = workPackage;
$scope.isWatched = !!workPackage.links.unwatch;
$scope.toggleWatchLink = workPackage.links.watch === undefined ? workPackage.links.unwatch : workPackage.links.watch;
$scope.watchers = workPackage.embedded.watchers;
}
function outputError(error) { function outputError(error) {
$scope.$emit('flashMessage', { $scope.$emit('flashMessage', {
isError: true, isError: true,
@ -68,38 +69,49 @@ angular.module('openproject.workPackages.controllers')
} }
$scope.outputError = outputError; // expose to child controllers $scope.outputError = outputError; // expose to child controllers
$scope.toggleWatch = function() { function setWorkPackageScopeProperties(workPackage){
$scope.toggleWatchLink $scope.workPackage = workPackage;
.fetch({ ajax: $scope.toggleWatchLink.props })
.then(refreshWorkPackage, outputError);
};
// resources for tabs
$scope.author = workPackage.embedded.author;
// activities and latest activities $scope.isWatched = !!workPackage.links.unwatch;
$scope.toggleWatchLink = workPackage.links.watch === undefined ? workPackage.links.unwatch : workPackage.links.watch;
$scope.watchers = workPackage.embedded.watchers;
$scope.activities = workPackage.embedded.activities; // activities and latest activities
$scope.activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically) $scope.activitiesSortedInDescendingOrder = ConfigurationService.commentsSortedInDescendingOrder();
$scope.activities = displayedActivities($scope.workPackage);
// watchers
$scope.latestActitivies = $scope.activities.reverse().slice(0, 3); // this leaves the activities in reverse order $scope.watchers = workPackage.embedded.watchers;
$scope.author = workPackage.embedded.author;
$scope.activitiesSortedInDescendingOrder = ConfigurationService.commentsSortedInDescendingOrder(); // Attachments
$scope.attachments = workPackage.embedded.attachments;
// restore former order of actvities unless comments are to be sorted in descending order // Author
if (!$scope.activitiesSortedInDescendingOrder) { $scope.author = workPackage.embedded.author;
$scope.activities.reverse();
} }
$scope.deleteWatcher = function(watcher) { $scope.toggleWatch = function() {
watcher.links.removeWatcher $scope.toggleWatchLink
.fetch({ ajax: watcher.links.removeWatcher.props }) .fetch({ ajax: $scope.toggleWatchLink.props })
.then(refreshWorkPackage, outputError); .then(refreshWorkPackage, outputError);
}; };
// Attachments function displayedActivities(workPackage) {
$scope.attachments = workPackage.embedded.attachments; var activities = workPackage.embedded.activities;
activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically)
if ($scope.activitiesSortedInDescendingOrder) {
activities.reverse();
}
return activities;
}
// toggles
$scope.toggleStates = {
hideFullDescription: true,
hideAllAttributes: true
};
$scope.editWorkPackage = function() { $scope.editWorkPackage = function() {
// TODO: Temporarily going to the old edit dialog until we get in-place editing done // TODO: Temporarily going to the old edit dialog until we get in-place editing done

@ -0,0 +1,43 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('editableComment', [function(){
return {
restrict: 'A',
scope: {
activity: '=',
commentInEdit: '='
},
templateUrl: '/templates/work_packages/tabs/_editable_comment.html',
link: function(scope){
}
};
}]);

@ -0,0 +1,50 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.tabs')
.directive('exclusiveEdit', function() {
return {
restrict: 'EA',
replace: true,
transclude: true,
template: '<div class="exclusive-edit" ng-transclude></div>',
controller: function() {
var editors = [];
this.gotEditable = function(selectedEditor) {
angular.forEach(editors, function(editor) {
if (selectedEditor != editor) {
editor.inEdit = false; }
});
};
this.addEditable = function(editor) {
editors.push(editor);
};
}
};
})

@ -28,25 +28,68 @@
angular.module('openproject.workPackages.tabs') angular.module('openproject.workPackages.tabs')
.directive('userActivity', ['I18n', 'PathHelper', function(I18n, PathHelper) { .directive('userActivity', ['$uiViewScroll', 'I18n', 'PathHelper', 'ActivityService', function($uiViewScroll, I18n, PathHelper, ActivityService) {
return { return {
restrict: 'E', restrict: 'E',
replace: true, replace: true,
require: '^?exclusiveEdit',
templateUrl: '/templates/work_packages/tabs/_user_activity.html', templateUrl: '/templates/work_packages/tabs/_user_activity.html',
scope: { scope: {
activity: '=', activity: '=',
currentAnchor: '=', activityNo: '=',
activityNo: '=' inputElementId: '='
}, },
link: function(scope) { link: function(scope, element, attrs, exclusiveEditController) {
exclusiveEditController.addEditable(scope);
scope.I18n = I18n; scope.I18n = I18n;
scope.userPath = PathHelper.staticUserPath; scope.userPath = PathHelper.staticUserPath;
scope.inEdit = false;
scope.inFocus = false;
scope.activity.links.user.fetch().then(function(user) { scope.activity.links.user.fetch().then(function(user) {
scope.userId = user.props.id; scope.userId = user.props.id;
scope.userName = user.props.name; scope.userName = user.props.name;
scope.userAvatar = user.props.avatar; scope.userAvatar = user.props.avatar;
}); });
scope.editComment = function() {
scope.inEdit = true;
exclusiveEditController.gotEditable(scope);
};
scope.cancelEdit = function() {
scope.inEdit = false;
};
scope.quoteComment = function() {
var elem = angular.element('#' + scope.inputElementId);
elem.val(quotedText(scope.activity.props.rawComment));
$uiViewScroll(elem);
};
scope.updateComment = function(comment) {
var comment = angular.element('#edit-comment-text').val();
ActivityService.updateComment(scope.activity.props.id, comment).then(function(activity){
scope.$emit('workPackageRefreshRequired', '');
scope.inEdit = false;
});
};
scope.showActions = function() {
scope.inFocus = true;
};
scope.hideActions = function() {
scope.inFocus = false;
};
function quotedText(rawComment) {
quoted = rawComment.split("\n")
.map(function(line){ return "\n> " + line; })
.join('');
return scope.userName + " wrote:" + quoted;
}
} }
}; };
}]); }]);

@ -43,6 +43,10 @@ See doc/COPYRIGHT.rdoc for more details.
cursor: pointer; cursor: pointer;
} }
.action-icon {
cursor: pointer;
}
select.to-validate.ng-dirty.ng-valid, input.to-validate.ng-dirty.ng-valid { border:1px solid Green; } select.to-validate.ng-dirty.ng-valid, input.to-validate.ng-dirty.ng-valid { border:1px solid Green; }
select.to-validate.ng-dirty.ng-invalid, input.to-validate.ng-dirty.ng-invalid { border:1px solid Red; } select.to-validate.ng-dirty.ng-invalid, input.to-validate.ng-dirty.ng-invalid { border:1px solid Red; }
select.to-validate.ng-dirty.ng-valid ~ span.ok, input.to-validate.ng-dirty.ng-valid ~ span.ok { color:green; display:inline; } select.to-validate.ng-dirty.ng-valid ~ span.ok, input.to-validate.ng-dirty.ng-valid ~ span.ok { color:green; display:inline; }

@ -0,0 +1,41 @@
/*-- copyright
* OpenProject is a project management system.
* Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 3.
*
* OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
* Copyright (C) 2006-2013 Jean-Philippe Lang
* Copyright (C) 2010-2013 the ChiliProject Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* See doc/COPYRIGHT.rdoc for more details. ++
*/
.activity-comment
textarea
width: 100%
height: 6em
-webkit-box-sizing: border-box
-moz-box-sizing: border-box
box-sizing: border-box
-webkit-border-radius: 3px
-moz-border-radius: 3px
border-radius: 3px
.button
float: right
margin-right: 0px

@ -56,6 +56,7 @@
@import content/work_packages @import content/work_packages
@import content/work_packages_filters @import content/work_packages_filters
@import content/work_packages_table @import content/work_packages_table
@import content/work_packages_details_activities
@import content/expandable_group_content @import content/expandable_group_content
@import content/control_colors @import content/control_colors
@import content/tabular @import content/tabular

@ -352,6 +352,9 @@ i
.work-package-details-activities-activity-contents .work-package-details-activities-activity-contents
padding: 10px 0 padding: 10px 0
textarea
width: 100%
resize: none
@media only screen and (max-width: 1280px) @media only screen and (max-width: 1280px)
.split-view .work-packages--details, .work-packages--split-view .work-packages--details .split-view .work-packages--details, .work-packages--split-view .work-packages--details

@ -21,8 +21,8 @@
"angular-truncate": "sparkalow/angular-truncate#fdf60fda265042d12e9414b5354b2cc52f1419de", "angular-truncate": "sparkalow/angular-truncate#fdf60fda265042d12e9414b5354b2cc52f1419de",
"angular-feature-flags": "mjt01/angular-feature-flags#ca40201e2279777dc3820c783f60f845cefff731", "angular-feature-flags": "mjt01/angular-feature-flags#ca40201e2279777dc3820c783f60f845cefff731",
"jquery-migrate": "~1.2.1", "jquery-migrate": "~1.2.1",
"momentjs": "~2.6.0", "momentjs": "~2.7.0",
"moment-timezone": "~0.0.6", "moment-timezone": "~0.2.0",
"angular-context-menu": "0.1.2", "angular-context-menu": "0.1.2",
"angular-busy": "~4.0.4", "angular-busy": "~4.0.4",
"hyperagent": "manwithtwowatches/hyperagent#v0.4.2" "hyperagent": "manwithtwowatches/hyperagent#v0.4.2"

@ -54,6 +54,8 @@ de:
general_text_No: "Nein" general_text_No: "Nein"
general_text_Yes: "Ja" general_text_Yes: "Ja"
label_add_columns: "Ausgewählte Spalten hinzufügen" label_add_columns: "Ausgewählte Spalten hinzufügen"
label_add_comment: "Kommentar hinzufügen"
label_add_comment_title: "Fügen Sie Ihre Kommentare hier zu"
label_added_by: "hinzugefügt von" label_added_by: "hinzugefügt von"
label_added_time_by: "Von %{author} %{age} hinzugefügt" label_added_time_by: "Von %{author} %{age} hinzugefügt"
label_ago: "vor" label_ago: "vor"

@ -54,6 +54,8 @@ en:
general_text_No: "No" general_text_No: "No"
general_text_Yes: "Yes" general_text_Yes: "Yes"
label_add_columns: "Add selected columns" label_add_columns: "Add selected columns"
label_add_comment: "Add comment"
label_add_comment_title: "Add your comments here"
label_added_by: "added by" label_added_by: "added by"
label_added_time_by: "Added by %{author} %{age}" label_added_time_by: "Added by %{author} %{age}"
label_ago: "days ago" label_ago: "days ago"

@ -29,12 +29,15 @@ module.exports = function(config) {
"vendor/assets/components/angular-truncate/src/truncate.js", "vendor/assets/components/angular-truncate/src/truncate.js",
"vendor/assets/components/angular-sanitize/angular-sanitize.js", "vendor/assets/components/angular-sanitize/angular-sanitize.js",
"vendor/assets/components/momentjs/moment.js", "vendor/assets/components/momentjs/moment.js",
"vendor/assets/components/moment-timezone/moment-timezone.js",
"vendor/assets/components/angular-context-menu/dist/angular-context-menu.js", "vendor/assets/components/angular-context-menu/dist/angular-context-menu.js",
'vendor/assets/components/select2/select2.js', 'vendor/assets/components/select2/select2.js',
'vendor/assets/components/hyperagent/dist/hyperagent.js', 'vendor/assets/components/hyperagent/dist/hyperagent.js',
"vendor/assets/components/openproject-ui_components/app/assets/javascripts/angular/ui-components-app.js", "vendor/assets/components/openproject-ui_components/app/assets/javascripts/angular/ui-components-app.js",
"vendor/assets/javascripts/moment-timezone/moment-timezone-data.js",
"app/assets/javascripts/angular/openproject-app.js", "app/assets/javascripts/angular/openproject-app.js",
"app/assets/javascripts/angular/config/work-packages-config.js", "app/assets/javascripts/angular/config/work-packages-config.js",
"app/assets/javascripts/angular/config/configuration-service.js", "app/assets/javascripts/angular/config/configuration-service.js",

@ -0,0 +1,55 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('Latest items filter', function() {
beforeEach(module('openproject.workPackages.filters'));
describe('latestItems', function() {
var items;
beforeEach(function(){
items = [1,2,3,4,5,6,7,8,9];
});
it('should be defined', inject(function($filter) {
expect($filter('latestItems')).not.to.equal(null);
}));
it('should return the first 3 items', inject(function($filter) {
expect($filter('latestItems')(items, false, 3)).to.eql([9,8,7]);
}));
it('should return the last 3 items reversed', inject(function($filter) {
expect($filter('latestItems')(items, true, 3)).to.eql([1,2,3]);
}));
});
});

@ -0,0 +1,82 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('ActivityService', function() {
var $httpBackend, ActivityService;
beforeEach(module('openproject.api', 'openproject.services', 'openproject.models'));
beforeEach(inject(function(_$httpBackend_, _ActivityService_) {
$httpBackend = _$httpBackend_;
ActivityService = _ActivityService_;
}));
describe('createComment', function() {
var setupFunction;
var workPackageId = 5;
var actvityId = 10;
var activities = [];
var descending = false;
var comment = "Jack Bauer 24 hour power shower";
var apiResource;
var apiFetchResource;
beforeEach(inject(function($q) {
apiResource = {
fetch: function() {
deferred = $q.defer();
deferred.resolve({ id: actvityId, comment: comment } );
return deferred.promise;
}
}
}));
beforeEach(inject(function(HALAPIResource) {
setupFunction = sinon.stub(HALAPIResource, 'setup').returns(apiResource);
}));
beforeEach(inject(function() {
apiFetchResource = ActivityService.createComment(workPackageId, activities, descending, comment);
}));
it('makes an api setup call', function() {
expect(setupFunction).to.have.been.calledWith("/work_packages/" + workPackageId + "/activities");
});
it('returns an activity', function() {
apiFetchResource.then(function(activity){
expect(activity.id).to.equal(activityId);
expect(activity.comment).to.equal(comment);
expect(activities.length).to.equal(1);
});
});
});
});

@ -30,11 +30,11 @@
describe('TimezoneService', function() { describe('TimezoneService', function() {
var TIME = '05/19/2014 11:49 AM'; var TIME = '2013-02-08T09:30:26';
var TimezoneService; var TimezoneService;
var ConfigurationService; var ConfigurationService;
var isTimezoneSetStub; var isTimezoneSetStub;
var timezone; var timezoneStub;
beforeEach(module('openproject.services', 'openproject.config')); beforeEach(module('openproject.services', 'openproject.config'));
@ -48,33 +48,24 @@ describe('TimezoneService', function() {
describe('#parseDate', function() { describe('#parseDate', function() {
it('is UTC', function() { it('is UTC', function() {
expect(TimezoneService.parseDate(TIME).zone()).to.equal(0); var time = TimezoneService.parseDate(TIME);
expect(time.zone()).to.equal(0);
expect(time.format("HH:mm")).to.eq("09:30");
}); });
describe('Non-UTC timezone', function() { describe('Non-UTC timezone', function() {
var timezone = 'Europe/Berlin'; var timezone = 'America/Vancouver';
var momentStub; var date;
var dateStub;
beforeEach(function() { beforeEach(function() {
isTimezoneSetStub.returns(true); isTimezoneSetStub.returns(true);
timezoneStub.returns(timezone); timezoneStub.returns(timezone);
momentStub = sinon.stub(moment, "utc"); date = TimezoneService.parseDate(TIME);
dateStub = sinon.stub();
momentStub.returns(dateStub);
dateStub.tz = sinon.spy();
TimezoneService.parseDate(TIME);
});
afterEach(function() {
momentStub.restore();
}); });
it('is Europe/Berlin', function() { it('is ' + timezone, function() {
expect(dateStub.tz.calledWithExactly(timezone)).to.be.true; expect(date.format("HH:mm")).to.eq("01:30");
}); });
}); });
}); });

@ -21,6 +21,31 @@ module API
@representer.to_json @representer.to_json
end end
helpers do
def save_activity(activity)
if activity.save
model = ::API::V3::Activities::ActivityModel.new(activity)
representer = ::API::V3::Activities::ActivityRepresenter.new(model)
representer.to_json
else
errors = activity.errors.full_messages.join(", ")
fail Errors::Validation.new(activity, description: errors)
end
end
end
params do
requires :comment, type: String
end
put do
authorize({ controller: :journals, action: :edit }, context: @activity.journable.project)
@activity.notes = params[:comment]
save_activity(@activity)
end
end end
end end

@ -80,10 +80,9 @@ module API
private private
def render_details(journal, no_html: false) def render_details(journal, no_html: false)
journal.details.map{ |d| journal.render_detail(d, no_html: no_html) } journal.details.map{ |d| journal.render_detail(d, no_html: no_html) }
end end
end end
end end
end end

@ -13,7 +13,7 @@ module API
before do before do
@work_package = WorkPackage.find(params[:id]) @work_package = WorkPackage.find(params[:id])
model = ::API::V3::WorkPackages::WorkPackageModel.new(work_package: @work_package) model = ::API::V3::WorkPackages::WorkPackageModel.new(work_package: @work_package)
@representer = ::API::V3::WorkPackages::WorkPackageRepresenter.new(model, current_user: current_user) @representer = ::API::V3::WorkPackages::WorkPackageRepresenter.new(model, { current_user: current_user }, :activities, :users)
end end
get do get do
@ -21,6 +21,35 @@ module API
@representer.to_json @representer.to_json
end end
resource :activities do
helpers do
def save_work_package(work_package)
if work_package.save
model = ::API::V3::Activities::ActivityModel.new(work_package.journals.last)
representer = ::API::V3::Activities::ActivityRepresenter.new(model)
representer.to_json
else
errors = work_package.errors.full_messages.join(", ")
fail Errors::Validation.new(work_package, description: errors)
end
end
end
params do
requires :comment, type: String
end
post do
authorize({ controller: :journals, action: :new }, context: @work_package.project)
@work_package.journal_notes = params[:comment]
save_work_package(@work_package)
end
end
mount ::API::V3::WorkPackages::WatchersAPI mount ::API::V3::WorkPackages::WatchersAPI
end end

@ -0,0 +1,7 @@
<div class="activity-comment">
<label>
<h3>{{ title }}</h3>
<textarea id="add-comment-text" name="add-comment"></textarea>
<button class="button" ng-click="createComment()">{{ buttonTitle }}</button>
</label>
</div>

@ -0,0 +1,5 @@
<span class="message"
ng-show="activity.props._type == 'Activity::Comment'"
ng-bind-html="activity.props.comment"/>
<textarea ng-if="commentInEdit = activity.props.id" id="edit-comment-text"></textarea>

@ -1,13 +1,32 @@
<div class="work-package-details-activities-activity-contents"> <div class="work-package-details-activities-activity-contents"
ng-mouseover="showActions()"
ng-mouseout="hideActions()">
<div class="comments-number"><a ng-href="#{{ currentAnchor }}" ng-bind="'#' + activityNo"></a> <div class="comments-number"><a ng-href="#{{ currentAnchor }}" ng-bind="'#' + activityNo"></a>
<div class="comments-icons"
ng-show="activity.props._type == 'Activity::Comment' && inFocus">
<i class="icon-quote action-icon" ng-click="quoteComment()"></i>
<i class="icon-edit action-icon" ng-click="editComment()"></i>
</div>
</div> </div>
<img class="avatar" ng-src="{{ userAvatar }}" /> <img class="avatar" ng-src="{{ userAvatar }}" />
<span class="user"><a ng-href="{{ userPath(userId) }}" name="{{ currentAnchor }}" ng-bind="userName"></a></span> <span class="user"><a ng-href="{{ userPath(userId) }}" name="{{ currentAnchor }}" ng-bind="userName"></a></span>
<span class="date">{{ I18n.t('js.label_commented_on') }} <date-time date-time-value="activity.props.createdAt"/></date-time> <span class="date">{{ I18n.t('js.label_commented_on') }} <date-time date-time-value="activity.props.createdAt"/></date-time>
<span class="comment wiki"> <span class="comment wiki">
<span class="message" <div ng-if="inEdit">
ng-show="activity.props._type == 'Activity::Comment'" <div>
ng-bind-html="activity.props.comment"/> <textarea id="edit-comment-text"
ng-bind-html="activity.props.rawComment"
rows="5"></textarea>
</div>
<div>
<button class="button" ng-click="updateComment()">{{ I18n.t('js.button_update') }}</button>
<span><a href="" ng-click="cancelEdit()">cancel</a></span>
</div>
</div>
<span ng-if="!inEdit"
class="message"
ng-show="activity.props._type == 'Activity::Comment'"
ng-bind-html="activity.props.comment"/>
<ul class="work-package-details-activities-messages"> <ul class="work-package-details-activities-messages">
<li ng-repeat="detail in activity.props.htmlDetails track by $index"> <li ng-repeat="detail in activity.props.htmlDetails track by $index">
<span class="message" ng-bind-html="detail"/> <span class="message" ng-bind-html="detail"/>

@ -1,19 +1,24 @@
<div class="detail-panel-content" > <div class="detail-panel-content" >
<div class="detail-activity"> <div class="detail-activity">
<ul class="work-package-details-activities-list"> <exclusive-edit>
<li ng-repeat="activity in activities" <ul class="work-package-details-activities-list">
class="work-package-details-activities-activity" <li ng-repeat="activity in activities"
ng-init="activityNo = activitiesSortedInDescendingOrder && activities.length - $index || $index + 1; class="work-package-details-activities-activity"
currentDate = (activity.props.createdAt|date:'longDate'); ng-init="activityNo = activitiesSortedInDescendingOrder && activities.length - $index || $index + 1;
currentAnchor = 'note-' + activityNo"> currentDate = (activity.props.createdAt|date:'longDate');
<h3 class="activity-date" currentAnchor = 'note-' + activityNo">
ng-if="currentDate !== (activities[$index-1].props.createdAt | date:'longDate')" <h3 class="activity-date"
ng-bind="currentDate"/> ng-if="currentDate !== (activities[$index-1].props.createdAt | date:'longDate')"
<user-activity activity="activity" ng-bind="currentDate"/>
activity-no="activityNo" <user-activity activity="activity"
current-anchor="currentAnchor"> activity-no="activityNo"
</user-activity> input-element-id="'add-comment-text'">
</li> </user-activity>
</ul> </li>
</ul>
</exclusive-edit>
</div> </div>
<activity-comment work-package="workPackage"
activities="activities">
</activity-comment>
</div> </div>

@ -37,14 +37,19 @@
<div class="detail-panel-latest-activity"> <div class="detail-panel-latest-activity">
<h3>{{ I18n.t('js.label_latest_activity') }}</h3> <h3>{{ I18n.t('js.label_latest_activity') }}</h3>
<ul> <exclusive-edit>
<li ng-repeat="activity in latestActitivies" <ul>
class="work-package-details-activities-activity" <li ng-repeat="activity in activities | latestItems:activitiesSortedInDescendingOrder:3"
ng-init="currentAnchor = 'note-' + ($index+1); class="work-package-details-activities-activity"
activityNo = activities.length - $index;"> ng-init="currentAnchor = 'note-' + ($index+1);
<user-activity activity="activity" activityNo = activities.length - $index;">
activity-no="activityNo" <user-activity activity="activity"
currentAnchor="currentAnchor"> activity-no="activityNo"
</li> input-element-id="'add-comment-text'">
</ul> </li>
</ul>
</exclusive-edit>
<activity-comment work-package="workPackage"
activities="activities">
</activity-comment>
</div> </div>

@ -0,0 +1,82 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe API::V3::Activities::ActivitiesAPI do
let(:admin) { FactoryGirl.create(:admin) }
let(:comment) { "This is a test comment!" }
shared_examples_for "safeguarded API" do
it { expect(response.response_code).to eq(403) }
end
shared_examples_for "valid activity request" do
before { allow(User).to receive(:current).and_return(admin) }
subject { JSON.parse(response.body) }
it { expect(subject['_type']).to eq("Activity::Comment") }
it { expect(subject['rawComment']).to eq(comment) }
end
shared_examples_for "invalid activity request" do
before { allow(User).to receive(:current).and_return(admin) }
it { expect(response.response_code).to eq(422) }
end
describe "PUT /api/v3/activities/:activityId" do
let(:work_package) { FactoryGirl.create(:work_package) }
let(:wp_journal) { FactoryGirl.build(:journal_work_package_journal) }
let(:journal) { FactoryGirl.create(:work_package_journal,
data: wp_journal,
journable_id: work_package.id) }
shared_context "edit activity" do
before { put "/api/v3/activities/#{journal.id}",
comment: comment }
end
it_behaves_like "safeguarded API" do
include_context "edit activity"
end
it_behaves_like "valid activity request" do
include_context "edit activity"
end
it_behaves_like "invalid activity request" do
before { allow_any_instance_of(Journal).to receive(:save).and_return(false) }
include_context "edit activity"
end
end
end

@ -0,0 +1,53 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
shared_examples_for "safeguarded API" do
it { expect(response.response_code).to eq(403) }
end
shared_examples_for "valid activity request" do
let(:status_code) { 200 }
before { allow(User).to receive(:current).and_return(admin) }
it { expect(response.response_code).to eq(status_code) }
describe 'response body' do
subject { JSON.parse(response.body) }
it { expect(subject['_type']).to eq("Activity::Comment") }
it { expect(subject['rawComment']).to eq(comment) }
end
end
shared_examples_for "invalid activity request" do
before { allow(User).to receive(:current).and_return(admin) }
it { expect(response.response_code).to eq(422) }
end

@ -0,0 +1,61 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe API::V3::WorkPackages::WorkPackagesAPI do
describe "activities" do
let(:admin) { FactoryGirl.create(:admin) }
let(:work_package) { FactoryGirl.create(:work_package) }
let(:comment) { "This is a test comment!" }
describe "POST /api/v3/work_packages/:id/activities" do
shared_context "create activity" do
before { post "/api/v3/work_packages/#{work_package.id}/activities",
comment: comment }
end
it_behaves_like "safeguarded API" do
include_context "create activity"
end
it_behaves_like "valid activity request" do
let(:status_code) { 201 }
include_context "create activity"
end
it_behaves_like "invalid activity request" do
before { allow_any_instance_of(WorkPackage).to receive(:save).and_return(false) }
include_context "create activity"
end
end
end
end

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save