Merge branch 'release/5.0' into dev

pull/3978/head
Jens Ulferts 9 years ago
commit 83f791a345
  1. 2
      Gemfile
  2. 4
      Gemfile.lock
  3. 6
      app/assets/stylesheets/_misc_legacy.sass
  4. 4
      app/controllers/watchers_controller.rb
  5. 70
      app/helpers/issues_helper.rb
  6. 4
      app/seeders/basic_data/builtin_roles_seeder.rb
  7. 4
      app/seeders/demo_data/project_seeder.rb
  8. 7
      app/views/timelog/edit.html.erb
  9. 28
      config/locales/en.seeders.yml
  10. 4
      config/locales/js-en.yml
  11. 1
      extra/Apache/OpenProjectRepoman.pm
  12. 4
      features/work_packages/editable_fields.feature
  13. 2
      features/work_packages/error_on_update.feature
  14. 4
      features/work_packages/switch_type.feature
  15. 4
      features/work_packages/update.feature
  16. 45
      frontend/app/components/common/services/loading-indicator.service.js
  17. 77
      frontend/app/components/routes/controllers/work-packages-list.controller.js
  18. 16
      frontend/app/components/routes/partials/work-packages.list.html
  19. 13
      frontend/app/components/routes/partials/work-packages.show.html
  20. 0
      frontend/app/components/wp-activity/activity-entry.directive.html
  21. 2
      frontend/app/components/wp-activity/activity-entry.directive.js
  22. 0
      frontend/app/components/wp-buttons/create-button/create-button.directive.html
  23. 4
      frontend/app/components/wp-buttons/create-button/create-button.directive.js
  24. 13
      frontend/app/components/wp-buttons/view-button/view-button.directive.html
  25. 69
      frontend/app/components/wp-buttons/view-button/view-button.directive.js
  26. 0
      frontend/app/components/wp-buttons/watcher-button/watcher-button.directive.html
  27. 3
      frontend/app/components/wp-buttons/watcher-button/watcher-button.directive.js
  28. 2
      frontend/app/components/wp-panels/watchers-panel/watchers-panel.directive.js
  29. 49
      frontend/app/components/wp-panels/watchers-panel/watchers-panel.directive.test.js
  30. 2
      frontend/app/templates/work_packages/activities/_link.html
  31. 6
      frontend/app/work_packages/controllers/work-packages-controller.js
  32. 85
      frontend/npm-shrinkwrap.json
  33. 1
      frontend/package.json
  34. 4
      frontend/scripts/clean-shrinkwrap.js
  35. 3
      frontend/webpack.config.js
  36. 3
      lib/open_project/version.rb
  37. 4
      lib/tabular_form_builder.rb
  38. 62
      spec/features/watching/toggle_watching_spec.rb
  39. 2
      spec/features/work_packages/details/activity_comments_spec.rb
  40. 2
      spec/features/work_packages/details/inplace_editor/version_editor_spec.rb
  41. 2
      spec/features/work_packages/new_work_package_spec.rb
  42. 171
      spec/features/work_packages/tabs/activity_tab_spec.rb
  43. 8
      spec/support/pages/full_work_package.rb
  44. 8
      spec/support/pages/split_work_package.rb

@ -193,7 +193,7 @@ group :development, :test do
gem 'pry-rescue'
gem 'pry-byebug', platforms: [:mri]
gem 'pry-doc'
gem 'parallel_tests'
gem 'parallel_tests', '~> 2.1.2'
gem 'rubocop', '~> 0.32'
end

@ -327,7 +327,7 @@ GEM
hashie (>= 1.2, < 4)
rack (~> 1.0)
parallel (1.6.1)
parallel_tests (1.6.1)
parallel_tests (2.1.2)
parallel
parser (2.2.2.5)
ast (>= 1.1, < 3.0)
@ -593,7 +593,7 @@ DEPENDENCIES
oj (~> 2.11.4)
omniauth
openproject-translations!
parallel_tests
parallel_tests (~> 2.1.2)
pg (~> 0.18.3)
poltergeist
prototype-rails!

@ -33,6 +33,8 @@
// placed elsewhere. Pleade DO NOT add to this file. Instead MOVE, refactor or
// REMOVE with ruthlessness.
$version-summary-width: 380px
#watchers
ul
margin: 0
@ -192,9 +194,11 @@ div
font-size: 120%
h2
font-size: 110%
.generic-table--no-results-container
max-width: calc(100% - #{$version-summary-width})
&#version-summary
float: right
width: 380px
width: $version-summary-width
margin-left: 16px
margin-bottom: 16px
background-color: #fff

@ -81,10 +81,6 @@ class WatchersController < ApplicationController
private
def find_watched_by_object
# Necessary check, otherwise anything can be constantized.
# The search types are plural, hence the `+ s`.
return false unless Redmine::Search.available_search_types.include?(params[:object_type] + 's')
klass = params[:object_type].singularize.camelcase.constantize
return false unless klass.respond_to?('watched_by') and

@ -30,17 +30,6 @@
module IssuesHelper
include ApplicationHelper
def issue_list(issues, &_block)
ancestors = []
issues.each do |issue|
while ancestors.any? && !issue.is_descendant_of?(ancestors.last)
ancestors.pop
end
yield issue, ancestors.size
ancestors << issue unless issue.leaf?
end
end
# Renders a HTML/CSS tooltip
#
# To use, a trigger div is needed. This is a div with the class of "tooltip"
@ -67,43 +56,6 @@ module IssuesHelper
<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe)
end
def render_descendants_tree(issue)
s = '<form><table class="list issues">'
issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
s << content_tag('tr',
content_tag('td',
"<label>#{l(:description_select_work_package) + ' #' + child.id.to_s}" +
check_box_tag('ids[]', child.id, false, id: nil) + '</label>',
class: 'checkbox') +
content_tag('td', link_to_issue(child, truncate: 60), class: 'subject') +
content_tag('td', h(child.status)) +
content_tag('td', link_to_user(child.assigned_to)) +
content_tag('td', progress_bar(child.done_ratio, width: '80px', legend: "#{child.done_ratio}%")),
class: "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
end
s << '</form></table>'
s
end
def render_custom_fields_rows(issue)
return if issue.custom_field_values.empty?
ordered_values = []
half = (issue.custom_field_values.size / 2.0).ceil
half.times do |i|
ordered_values << issue.custom_field_values[i]
ordered_values << issue.custom_field_values[i + half]
end
s = "<tr>\n"
n = 0
ordered_values.compact.each do |value|
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
s << "\t<th>#{h(value.custom_field.name)}:</th><td>#{simple_format_without_paragraph(h(show_value(value)))}</td>\n"
n += 1
end
s << "</tr>\n"
s.html_safe
end
# Find the name of an associated record stored in the field attribute
def find_name_by_reflection(field, id)
association = WorkPackage.reflect_on_association(field.to_sym)
@ -113,34 +65,12 @@ module IssuesHelper
end
end
# Renders issue children recursively
def render_api_issue_children(issue, api)
return if issue.leaf?
api.array :children do
issue.children.each do |child|
api.issue(id: child.id) do
api.type(id: child.type_id, name: child.type.name) unless child.type.nil?
api.subject child.subject
render_api_issue_children(child, api)
end
end
end
end
def entries_for_filter_select_sorted(query)
[['', '']] + query.available_work_package_filters.map { |field| [field[1][:name] || WorkPackage.human_attribute_name(field[0]), field[0]] unless query.has_filter?(field[0]) }.compact.sort_by { |el|
ActiveSupport::Inflector.transliterate(el[0]).downcase
}
end
def value_overridden_by_children?(attrib)
WorkPackage::ATTRIBS_WITH_VALUES_FROM_CHILDREN.include? attrib
end
def attrib_disabled?(issue, attrib)
value_overridden_by_children?(attrib) && !(issue.new_record? || issue.leaf?)
end
def last_issue_note(issue)
note_journals = issue.journals.select(&:notes?)
return t(:text_no_notes) if note_journals.empty?

@ -41,8 +41,8 @@ module BasicData
def data
[
{name: 'Non member', position: 0, builtin: Role::BUILTIN_NON_MEMBER },
{name: 'Anonymous', position: 1, builtin: Role::BUILTIN_ANONYMOUS }
{ name: I18n.t(:default_role_non_member), position: 0, builtin: Role::BUILTIN_NON_MEMBER },
{ name: I18n.t(:default_role_anonymous), position: 1, builtin: Role::BUILTIN_ANONYMOUS }
]
end
end

@ -112,8 +112,8 @@ module DemoData
version_data.each do |attributes|
project.versions << Version.create!(
name: attributes[:name],
status: I18n.t(attributes[:status]),
sharing: I18n.t(attributes[:sharing])
status: attributes[:status],
sharing: attributes[:sharing]
)
end
end

@ -40,12 +40,12 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
<div class="form--field -required">
<%= f.text_field :spent_on, size: 10 %>
<%= f.text_field :spent_on, size: 10, required: true %>
<%= calendar_for('time_entry_spent_on') %>
</div>
<div class="form--field -required">
<%= f.text_field :hours, size: 6 %>
<%= f.text_field :hours, size: 6, required: true %>
</div>
<div class="form--field">
@ -54,7 +54,8 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field -required">
<%= f.select :activity_id,
activity_collection_for_select_options(@time_entry) %>
activity_collection_for_select_options(@time_entry),
required: true %>
</div>
<%= render partial: "customizable/form",

@ -39,26 +39,26 @@ en:
description: 'This is a description for your project. You can edit the description in the Project settings -> Description'
versions:
- name: '1.0'
sharing: :label_none
status: :version_status_open
sharing: 'none'
status: 'open'
- name: '1.1'
sharing: :label_none
status: :version_status_open
sharing: 'none'
status: 'open'
- name: '2.0'
sharing: :label_none
status: :version_status_open
sharing: 'none'
status: 'open'
- name: Product Backlog
sharing: :label_none
status: :version_status_open
sharing: 'none'
status: 'open'
- name: Sprint 1
sharing: :label_none
status: :version_status_open
sharing: 'none'
status: 'open'
- name: Sprint 2
sharing: :label_none
status: :version_status_open
sharing: 'none'
status: 'open'
- name: Wish List
sharing: :label_none
status: :version_status_open
sharing: 'none'
status: 'open'
timeline:
name: Timeline

@ -111,7 +111,7 @@ en:
label_format_pdf_with_descriptions: "PDF with descriptions"
label_greater_or_equal: ">="
label_group_by: "Group by"
label_hide_attributes: "Hide empty"
label_hide_attributes: "Hide empty attributes"
label_hide_column: "Hide column"
label_in: "in"
label_in_less_than: "in less than"
@ -143,7 +143,7 @@ en:
label_select_watcher: "Select a watcher..."
label_selected_filter_list: "Selected filters"
label_share: "Share"
label_show_attributes: "Show all"
label_show_attributes: "Show all attributes"
label_show_in_menu: "Show page in menu"
label_sort_by: "Sort by"
label_sorted_by: "sorted by"

@ -7,7 +7,6 @@ use File::Path qw(rmtree);
use File::Spec ();
use File::Copy qw(move);
use Apache2::Module;
use Apache2::Module;
use Apache2::Access;
use Apache2::ServerRec qw();

@ -68,7 +68,7 @@ Feature: Fields editable on work package edit
When I go to the edit page of the work package called "pe1"
And I click the edit work package button
And I click on "Show all"
And I click on "Show all attributes"
Then I should see the following fields:
| Type | Phase |
@ -112,7 +112,7 @@ Feature: Fields editable on work package edit
When I go to the edit page of the work package called "pe1"
And I click the edit work package button
And I click on "Show all"
And I click on "Show all attributes"
Then I should see the following fields:
| cf1 | 4 |

@ -54,7 +54,7 @@ Feature: Error messages are displayed
Scenario: Inserting a too long subject results in an error beeing shown
When I go to the edit page of the work package called "pe1"
And I click the edit work package button
And I click on "Show all"
And I click on "Show all attributes"
And I fill in the following:
| Subject | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. mollit anim id est laborum. |
And I submit the form by the "Save" button

@ -59,7 +59,7 @@ Feature: Switching types of work packages
Scenario: Switching type should keep the inserted value
When I go to the edit page of the work package "wp1"
And I click the edit work package button
And I click on "Show all"
And I click on "Show all attributes"
And I fill in the following:
| Responsible | Bob Bobbit |
And I select "Feature" from "Type"
@ -82,7 +82,7 @@ Feature: Switching types of work packages
When I go to the edit page of the work package "wp1"
And I click the edit work package button
And I click on "Show all"
And I click on "Show all attributes"
And I fill in the following:
| cfAll | 5 |
And I select "Feature" from "Type"

@ -77,7 +77,7 @@ Feature: Updating work packages
Scenario: Updating the work package and seeing the results on the show page
When I go to the edit page of the work package called "wp1"
And I click the edit work package button
And I click on "Show all"
And I click on "Show all attributes"
And I fill in the following:
| Type | Phase2 |
# This is to be removed once the bug
@ -130,7 +130,7 @@ Feature: Updating work packages
| child |
When I go to the edit page of the work package "parent"
And I click the edit work package button
And I click on "Show all"
And I click on "Show all attributes"
Then the work package should be shown with the following values:
| Priority | prio2 |
| Date | 10/01/2015 - 10/30/2015 |

@ -0,0 +1,45 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.services')
.factory('loadingIndicator', loadingIndicator);
function loadingIndicator() {
var config = {};
return {
get config() {
return config;
},
on: function (promise) {
config.promise = promise;
}
};
}

@ -31,11 +31,13 @@ angular
.controller('WorkPackagesListController', WorkPackagesListController);
function WorkPackagesListController($scope, $rootScope, $state, $stateParams, $location, latestTab,
I18n, WorkPackagesTableService, WorkPackageService, ProjectService, QueryService,
WorkPackagesTableService, WorkPackageService, ProjectService, QueryService,
PaginationService, AuthorisationService, UrlParamsHelper, Query,
OPERATORS_AND_LABELS_BY_FILTER_TYPE, NotificationsService, EditableFieldsState) {
OPERATORS_AND_LABELS_BY_FILTER_TYPE, NotificationsService, EditableFieldsState,
loadingIndicator) {
$scope.projectIdentifier = $stateParams.projectPath || null;
$scope.loadingIndicator = loadingIndicator;
// Setup
function initialSetup() {
@ -65,15 +67,17 @@ function WorkPackagesListController($scope, $rootScope, $state, $stateParams, $l
fetchWorkPackages = WorkPackageService.getWorkPackages($scope.projectIdentifier);
}
$scope.settingUpPage = fetchWorkPackages // put promise in scope for cg-busy
.then(function(json) {
return setupPage(json, !!queryParams);
})
.then(function() {
var promise = fetchWorkPackages.then(function(json) {
return setupPage(json, !!queryParams);
}).then(function() {
fetchAvailableColumns();
fetchProjectQueries();
QueryService.loadAvailableGroupedQueries($scope.projectIdentifier);
});
}
);
loadingIndicator.on(promise);
}
function fetchWorkPackagesFromUrlParams(queryParams) {
@ -215,8 +219,7 @@ function WorkPackagesListController($scope, $rootScope, $state, $stateParams, $l
// Clear unsaved changes to current query
clearUrlQueryParams();
// Load new query
$scope.settingUpPage = $state.go('work-packages.list', { 'query_id': queryId });
loadingIndicator.on($state.go('work-packages.list', { 'query_id': queryId }));
};
function updateResults() {
@ -271,22 +274,24 @@ function WorkPackagesListController($scope, $rootScope, $state, $stateParams, $l
return selected || $scope.rows.first().object.id;
}
$scope.nextAvailableWorkPackage = nextAvailableWorkPackage;
$scope.openLatestTab = function() {
$scope.settingUpPage = $state.go(
latestTab.getStateName(),
{
workPackageId: nextAvailableWorkPackage(),
'query_props': $location.search()['query_props']
});
var promise = $state.go(latestTab.getStateName(), {
workPackageId: nextAvailableWorkPackage(),
'query_props': $location.search()['query_props']
});
loadingIndicator.on(promise);
};
$scope.openOverviewTab = function() {
$scope.settingUpPage = $state.go(
'work-packages.list.details.overview',
{
workPackageId: nextAvailableWorkPackage(),
'query_props': $location.search()['query_props']
});
var promise = $state.go('work-packages.list.details.overview', {
workPackageId: nextAvailableWorkPackage(),
'query_props': $location.search()['query_props']
});
loadingIndicator.on(promise);
};
$scope.closeDetailsView = function() {
@ -297,32 +302,12 @@ function WorkPackagesListController($scope, $rootScope, $state, $stateParams, $l
$scope.showWorkPackageDetails = function(id, force) {
if (force || $state.current.url != "") {
$scope.settingUpPage = $state.go(
latestTab.getStateName(),
{ workPackageId: id, 'query_props': $location.search()['query_props'] }
);
}
};
$scope.showWorkPackageShowView = function() {
if (EditableFieldsState.editAll.state && $state.params.type) {
$state.go('work-packages.new', $state.params);
} else {
var id = $state.params.workPackageId
|| $scope.preselectedWorkPackageId || nextAvailableWorkPackage(),
// Have to use $location.search() here as $state.params
// isn't filled unless the url is queried for by the
// browser. This seems to be caused by #maintainUrlQueryState
// where we set the search via $location.search.
queryProps = $location.search()['query_props'];
$state.go('work-packages.show.activity', {
projectPath: $scope.projectIdentifier || '',
var promise = $state.go(latestTab.getStateName(), {
workPackageId: id,
'query_props': queryProps
'query_props': $location.search()['query_props']
});
loadingIndicator.on(promise);
}
};

@ -60,18 +60,7 @@
</button>
</li>
<li>
<label for="work-packages-show-view-button"
ng-attr-accesskey="{{ isShowViewActive() ? undefined : '9' }}"
class="hidden-for-sighted">
{{ getActivationActionLabel(!isShowViewActive()) + ' ' + I18n.t('js.button_show_view') }}
</label>
<button id="work-packages-show-view-button"
class="button"
title="{{ getActivationActionLabel(!isShowViewActive()) + ' ' + I18n.t('js.button_show_view') }}"
ng-click="showWorkPackageShowView()"
ng-class="{ '-active': isShowViewActive() }">
<i class="icon-fullscreen-view button--icon"></i>
</button>
<wp-view-button></wp-view-button>
</li>
</ul>
</li>
@ -102,8 +91,7 @@
<back-url></back-url>
<div class="work-packages--split-view" cg-busy="[settingUpPage,refreshWorkPackages]"
<div class="work-packages--split-view" cg-busy="loadingIndicator.config"
ng-class="{'edit-all-mode': editAll.state}">
<div class="work-packages--list">
<div class="work-packages--list-table-area">

@ -53,18 +53,7 @@
</button>
</li>
<li>
<label for="work-packages-show-view-button"
ng-attr-accesskey="{{ isShowViewActive() ? undefined : '9' }}"
class="hidden-for-sighted">
{{ getActivationActionLabel(!isShowViewActive()) + ' ' + I18n.t('js.button_show_view') }}
</label>
<button id="work-packages-show-view-button"
class="button"
title="{{ getActivationActionLabel(!isShowViewActive()) + ' ' + I18n.t('js.button_show_view') }}"
ng-click="showWorkPackageShowView()"
ng-class="{ '-active': isShowViewActive() }">
<i class="icon-fullscreen-view button--icon"></i>
</button>
<wp-view-button></wp-view-button>
</li>
</ul>
</li>

@ -34,7 +34,7 @@ function activityEntry(PathHelper) {
return {
restrict: 'E',
replace: true,
templateUrl: '/components/work-packages/activity/activity-entry.directive.html',
templateUrl: '/components/wp-activity/activity-entry.directive.html',
scope: {
workPackage: '=',

@ -33,8 +33,7 @@ angular
function wpCreateButton() {
return {
restrict: 'E',
templateUrl: '/components/work-packages/directives/wp-create-button/' +
'wp-create-button.directive.html',
templateUrl: '/components/wp-buttons/create-button/create-button.directive.html',
scope: {
projectIdentifier: '=',
@ -48,7 +47,6 @@ function wpCreateButton() {
}
function WorkPackageCreateButtonController($state, ProjectService) {
var vm = this,
inProjectContext = !!vm.projectIdentifier,
canCreate= false;

@ -0,0 +1,13 @@
<label for="work-packages-show-view-button"
ng-attr-accesskey="{{ accessKey }}"
class="hidden-for-sighted">
{{ label }}
</label>
<button id="work-packages-show-view-button"
class="button"
title="{{ label }}"
ng-click="showWorkPackageShowView()"
ng-class="{ '-active': isShowViewActive() }">
<i class="icon-fullscreen-view button--icon"></i>
</button>

@ -0,0 +1,69 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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('wpViewButton', wpViewButton);
function wpViewButton() {
return {
restrict: 'E',
templateUrl: '/components/wp-buttons/view-button/view-button.directive.html',
controller: WorkPackageViewButtonController
};
}
function WorkPackageViewButtonController($scope, $state, $location) {
$scope.isShowViewActive = function() {
return $state.includes('work-packages.show');
};
$scope.label = $scope.getActivationActionLabel(!$scope.isShowViewActive())
+ I18n.t('js.button_show_view');
if ($scope.isShowViewActive()) {
$scope.accessKey = 9;
}
$scope.showWorkPackageShowView = function() {
if ($state.is('work-packages.list.new') && $state.params.type) {
$state.go('work-packages.new', $state.params);
} else {
var id = $state.params.workPackageId || $scope.preselectedWorkPackageId ||
$scope.nextAvailableWorkPackage(), queryProps = $location.search()['query_props'];
$state.go('work-packages.show.activity', {
projectPath: $scope.projectIdentifier || '',
workPackageId: id,
'query_props': queryProps
});
}
};
}

@ -33,8 +33,7 @@ angular
function wpWatcherButton() {
return {
replace: true,
templateUrl: '/components/work-packages/directives/wp-watcher-button/' +
'wp-watcher-button.directive.html',
templateUrl: '/components/wp-buttons/watcher-button/watcher-button.directive.html',
scope: {
workPackage: '=',

@ -33,7 +33,7 @@ angular
function watchersPanel() {
return {
restrict: 'E',
templateUrl: '/components/wp-panels/directives/watchers-panel.directive.html',
templateUrl: '/components/wp-panels/watchers-panel/watchers-panel.directive.html',
scope: {
workPackage: '='
},

@ -0,0 +1,49 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.
// ++
describe('Watchers panel directive', function () {
var $compile, $rootScope, element;
beforeEach(angular.mock.module('openproject.workPackages.controllers', function ($controllerProvider) {
$controllerProvider.register('WatchersPanelController', function () {});
}));
beforeEach(angular.mock.module('openproject.templates'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
element = $compile('<watchers-panel work-package="workPackage"></watchers-panel>')($rootScope);
$rootScope.$digest();
}));
it('should should be rendered correctly', function () {
expect(element.html()).to.contain('detail-panel-watchers');
});
});

@ -1,4 +1,4 @@
<a id ="{{ activityHtmlId }}"
<a id ="{{ activityHtmlId }}-link"
ng-bind="'#' + activityNo"
tabindex="-1"
ui-sref="work-packages.show.activity({ workPackageId: workPackageId, '#': activityHtmlId})"></a>

@ -50,16 +50,12 @@ module.exports = function($scope, $state, $stateParams, QueryService, PathHelper
return $state.is('work-packages.list');
};
$scope.isShowViewActive = function() {
return $state.includes('work-packages.show');
};
$scope.getToggleActionLabel = function(active) {
return (active) ? I18n.t('js.label_deactivate') : I18n.t('js.label_activate');
};
$scope.getActivationActionLabel = function(activate) {
return (activate) ? I18n.t('js.label_activate') : '';
return (activate) ? I18n.t('js.label_activate') + ' ' : '';
};
$rootScope.$broadcast('openproject.layout.activateMenuItem');
};

@ -1219,7 +1219,82 @@
"version": "0.5.1"
},
"lodash": {
"version": "2.4.1"
"version": "2.4.2"
},
"ng-annotate-loader": {
"version": "0.0.10",
"dependencies": {
"ng-annotate": {
"version": "1.0.2",
"dependencies": {
"acorn": {
"version": "2.1.0"
},
"alter": {
"version": "0.2.0"
},
"convert-source-map": {
"version": "1.0.0"
},
"optimist": {
"version": "0.6.1",
"dependencies": {
"wordwrap": {
"version": "0.0.3"
},
"minimist": {
"version": "0.0.10"
}
}
},
"ordered-ast-traverse": {
"version": "1.1.1",
"dependencies": {
"ordered-esprima-props": {
"version": "1.1.0"
}
}
},
"simple-fmt": {
"version": "0.1.0"
},
"simple-is": {
"version": "0.2.0"
},
"stable": {
"version": "0.1.5"
},
"stringmap": {
"version": "0.2.2"
},
"stringset": {
"version": "0.2.1"
},
"tryor": {
"version": "0.1.2"
}
}
},
"source-map": {
"version": "0.4.2",
"dependencies": {
"amdefine": {
"version": "1.0.0"
}
}
},
"loader-utils": {
"version": "0.2.12",
"dependencies": {
"big.js": {
"version": "3.1.3"
},
"json5": {
"version": "0.4.0"
}
}
}
}
},
"ngtemplate-loader": {
"version": "0.1.3",
@ -1683,14 +1758,6 @@
}
}
}
},
"fsevents": {
"version": "0.3.6",
"dependencies": {
"nan": {
"version": "1.8.4"
}
}
}
}
},

@ -56,6 +56,7 @@
"html-loader": "^0.2.3",
"json-loader": "^0.5.1",
"lodash": "^2.4.2",
"ng-annotate-loader": "0.0.10",
"ngtemplate-loader": "^0.1.2",
"polyfill-function-prototype-bind": "0.0.1",
"shelljs": "^0.3.0",

@ -1,3 +1,5 @@
#!/usr/bin/env node
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
@ -26,8 +28,6 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
#!/usr/bin/env node
/**
* this script is just a temporary solution to deal with the issue of npm outputting the npm
* shrinkwrap file in an unstable manner.

@ -66,7 +66,8 @@ var loaders = [
{ test: /\.png$/, loader: 'url-loader?limit=100000&mimetype=image/png' },
{ test: /\.gif$/, loader: 'file-loader' },
{ test: /\.jpg$/, loader: 'file-loader' },
{ test: /js-[\w|-]{2,5}\.yml$/, loader: 'json!yaml' }
{ test: /js-[\w|-]{2,5}\.yml$/, loader: 'json!yaml' },
{ test: /[\/].*\.js$/, loader: 'ng-annotate?map=true' }
];
for (var k in pathConfig.pluginNamesPaths) {

@ -28,6 +28,7 @@
#++
require 'rexml/document'
require 'open3'
module OpenProject
module VERSION #:nodoc:
@ -52,7 +53,7 @@ module OpenProject
end
def self.revision
revision = `git rev-parse HEAD`
revision, = Open3.capture3('git', 'rev-parse', 'HEAD')
if revision.present?
revision.strip[0..8]
end

@ -72,6 +72,10 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
def select(field, choices, options = {}, html_options = {})
html_options[:class] = Array(html_options[:class]) + %w(form--select)
if options[:required]
html_options[:required] = true
end
label_for_field(field, options) + container_wrap_field(super, 'select', options)
end

@ -0,0 +1,62 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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'
describe 'Toggle watching', type: :feature, js: true do
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: [:view_messages, :view_wiki_pages]) }
let(:user) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
let(:news) { FactoryGirl.create(:news, project: project) }
let(:board) { FactoryGirl.create(:board, project: project) }
let(:message) { FactoryGirl.create(:message, board: board) }
let(:wiki) { project.wiki }
let(:wiki_page) { FactoryGirl.create(:wiki_page_with_content, wiki: wiki) }
before do
allow(User).to receive(:current).and_return user
end
it 'can toggle watch and unwatch' do
# Work packages have a different toggle and are hence not considered here
[news_path(news),
project_board_path(project, board),
topic_path(message),
project_wiki_path(project, wiki_page)].each do |path|
visit path
click_link(I18n.t('button_watch'))
expect(page).to have_link(I18n.t('button_unwatch'))
click_link(I18n.t('button_unwatch'))
expect(page).to have_link(I18n.t('button_watch'))
end
end
end

@ -134,7 +134,7 @@ describe 'activity comments', js: true, selenium: true do
text: initial_comment)
# Hover comment
page.find('.work-package-details-activities-activity-contents').hover
page.find('.user-comment > .message').hover
# Quote this comment
page.find('.comments-icons .icon-quote', visible: false).click

@ -38,7 +38,7 @@ describe 'subject inplace editor', js: true, selenium: true do
login_as(user)
work_packages_page.visit_index(work_package)
within '.work-packages--details-content' do
click_on 'Show all'
click_on 'Show all attributes'
end
field.activate_edition
end

@ -132,7 +132,7 @@ describe 'new work package', js: true do
it do
within '.panel-toggler' do
click_on 'Show all'
click_on 'Show all attributes'
end
ids = custom_fields.map(&:id)

@ -0,0 +1,171 @@
require 'spec_helper'
require 'features/work_packages/work_packages_page'
require 'features/work_packages/details/inplace_editor/work_package_field'
describe 'Activity tab', js: true, selenium: true do
def alter_work_package_at(work_package, attributes:, at:, user: User.current)
work_package.update_attributes(attributes.merge({ updated_at: at }))
note_journal = work_package.journals.last
note_journal.update_attributes(created_at: at, user: attributes[:user])
end
let(:project) { FactoryGirl.create :project_with_types, is_public: true }
let!(:work_package) {
work_package = FactoryGirl.create(:work_package,
project: project,
created_at: 5.days.ago.to_date.to_s(:db),
subject: initial_subject,
journal_notes: initial_comment)
note_journal = work_package.journals.last
note_journal.update_attributes(created_at: 5.days.ago.to_date.to_s)
work_package
}
let(:initial_subject) { 'My Subject' }
let(:initial_comment) { 'First comment on this wp.' }
let(:comments_in_reverse) { false }
let(:initial_note) {
work_package.journals[0]
}
let!(:note_1) {
attributes = { subject: 'New subject',
description: 'Some not so long description.',
journal_notes: 'Updated the subject and description' }
alter_work_package_at(work_package,
attributes: attributes,
at: 3.days.ago.to_date.to_s(:db),
user: user)
work_package.journals.last
}
let!(:note_2) {
attributes = { journal_notes: 'Another comment by a different user' }
alter_work_package_at(work_package,
attributes: attributes,
at: 1.days.ago.to_date.to_s(:db),
user: FactoryGirl.create(:admin))
work_package.journals.last
}
before do
login_as(user)
allow(user.pref).to receive(:warn_on_leaving_unsaved?).and_return(false)
allow(user.pref).to receive(:comments_in_reverse_order?).and_return(comments_in_reverse)
end
shared_examples 'shows activities in order' do
let(:journals) {
journals = [initial_note, note_1, note_2]
journals
}
it 'shows activities in ascending order' do
journals.each_with_index do |journal, idx|
date_selector = ".work-package-details-activities-activity:nth-of-type(#{idx + 1}) " +
'.activity-date'
expect(page).to have_selector(date_selector,
text: journal.created_at.to_date.to_s(:long))
activity = page.find("#activity-#{idx + 1}")
expect(activity).to have_selector('.user', text: journal.user.name)
expect(activity).to have_selector('.user-comment > .message', text: journal.notes)
if activity == note_1
expect(activity).to have_selector('.work-package-details-activities-messages .message',
count: 2)
expect(activity).to have_selector('.message',
text: "Subject changed from #{initial_subject} " \
"to #{journal.data.subject}")
end
end
end
end
shared_examples 'activity tab' do
before do
work_package_page.visit_tab! 'activity'
expect(page).to have_selector('.user-comment > .message',
text: initial_comment)
end
context 'with permission' do
let(:role) {
FactoryGirl.create(:role, permissions: [:view_work_packages,
:add_work_package_notes])
}
let(:user) {
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
}
context 'with ascending comments' do
let(:comments_in_reverse) { false }
it_behaves_like 'shows activities in order'
end
context 'with reversed comments' do
let(:comments_in_reverse) { true }
it_behaves_like 'shows activities in order'
end
it 'can quote a previous comment' do
# Hover comment
page.find('#activity-1 .work-package-details-activities-activity-contents').hover
# Quote this comment
page.find('#activity-1 .comments-icons .icon-quote', visible: false).click
field = WorkPackageField.new(page, 'activity', '.work-packages--activity--add-comment')
expect(field.editing?).to be true
# Add our comment
quote = field.input_element[:value]
expect(quote).to include("> #{initial_comment}")
quote << "\nthis is some remark under a quote"
field.input_element.set(quote)
field.submit_by_click
expect(page).to have_selector('.user-comment > .message', count: 4)
expect(page).to have_selector('.user-comment > .message blockquote')
end
end
context 'with no permission' do
let(:role) {
FactoryGirl.create(:role, permissions: [:view_work_packages])
}
let(:user) {
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
}
it 'shows the activities, but does not allow commenting' do
expect(page).not_to have_selector('.work-packages--activity--add-comment', visible: true)
end
end
end
context 'split screen' do
let(:work_package_page) { Pages::SplitWorkPackage.new(work_package, project) }
it_behaves_like 'activity tab'
end
context 'full screen' do
let(:work_package_page) { Pages::FullWorkPackage.new(work_package) }
it_behaves_like 'activity tab'
end
end

@ -62,14 +62,18 @@ module Pages
end
end
def visit_tab!(tab)
visit path(tab)
end
private
def container
find('.work-packages--show-view')
end
def path
work_package_path(work_package.id, 'activity')
def path(tab='activity')
work_package_path(work_package.id, tab)
end
end
end

@ -50,14 +50,18 @@ module Pages
expect(current_path).to eql path
end
def visit_tab!(tab)
visit path(tab)
end
private
def details_container
find('.work-packages--details')
end
def path
state = "#{work_package.id}/overview"
def path(tab='overview')
state = "#{work_package.id}/#{tab}"
if project
project_work_packages_path(project, "details/#{state}")

Loading…
Cancel
Save