merge current dev

pull/6274/head
Wieland Lindenthal 7 years ago
commit 386f550e04
  1. 26
      .travis.yml
  2. 18
      Gemfile
  3. 34
      Gemfile.lock
  4. 1
      app/assets/javascripts/jstoolbar.js
  5. 193
      app/assets/javascripts/jstoolbar/markdown.js
  6. 2
      app/assets/javascripts/jstoolbar/textile.js
  7. 2
      app/assets/javascripts/styleguide.js
  8. 5
      app/assets/stylesheets/content/_forms.sass
  9. 2
      app/assets/stylesheets/content/_modal.sass
  10. 2
      app/assets/stylesheets/content/_tabs.sass
  11. 7
      app/assets/stylesheets/content/work_packages/_table_hierarchy.sass
  12. 3
      app/assets/stylesheets/content/work_packages/single_view/_single_view.sass
  13. 19
      app/assets/stylesheets/layout/_work_package_table_embedded.sass
  14. 1
      app/cells/views/settings/text_setting/show.erb
  15. 9
      app/controllers/wiki_controller.rb
  16. 6
      app/helpers/text_formatting_helper.rb
  17. 5
      app/helpers/types_helper.rb
  18. 2
      app/models/mail_handler.rb
  19. 8
      app/models/queries/relations/filters/from_filter.rb
  20. 24
      app/models/queries/relations/filters/involved_filter.rb
  21. 4
      app/models/queries/relations/filters/relation_filter.rb
  22. 12
      app/models/queries/relations/filters/to_filter.rb
  23. 62
      app/models/queries/relations/filters/visibility_checking.rb
  24. 12
      app/models/queries/relations/relation_query.rb
  25. 2
      app/models/time_entry.rb
  26. 43
      app/models/type/attribute_groups.rb
  27. 9
      app/models/type/attributes.rb
  28. 25
      app/models/work_package.rb
  29. 8
      app/policies/query_policy.rb
  30. 4
      app/seeders/development_data/custom_fields_seeder.rb
  31. 6
      app/seeders/root_seeder.rb
  32. 2
      app/services/api/v3/parse_query_params_service.rb
  33. 13
      app/services/base_type_service.rb
  34. 2
      app/views/my/access_token.html.erb
  35. 4
      app/views/onboarding/_menu_item.html.erb
  36. 3
      app/views/projects/form/attributes/_description.html.erb
  37. 15
      app/views/settings/_general.html.erb
  38. 2
      app/views/types/form/_form_configuration.html.erb
  39. 2
      app/views/types/form/_query_group.html.erb
  40. 5
      config/locales/en.yml
  41. 2
      config/locales/js-en.yml
  42. 2
      config/settings.yml
  43. 2
      docs/configuration/configuration.md
  44. 4
      features/step_definitions/board_steps.rb
  45. 2
      features/step_definitions/common_steps.rb
  46. 2
      features/step_definitions/custom_field_steps.rb
  47. 26
      features/step_definitions/general_steps.rb
  48. 6
      features/step_definitions/group_steps.rb
  49. 4
      features/step_definitions/issue_category_steps.rb
  50. 10
      features/step_definitions/issue_steps.rb
  51. 4
      features/step_definitions/priority_steps.rb
  52. 2
      features/step_definitions/project_steps.rb
  53. 4
      features/step_definitions/query_steps.rb
  54. 2
      features/step_definitions/repository_steps.rb
  55. 6
      features/step_definitions/role_steps.rb
  56. 2
      features/step_definitions/search_steps.rb
  57. 2
      features/step_definitions/status_steps.rb
  58. 4
      features/step_definitions/time_entry_steps.rb
  59. 2
      features/step_definitions/type_steps.rb
  60. 4
      features/step_definitions/user_steps.rb
  61. 2
      features/step_definitions/version_steps.rb
  62. 16
      features/step_definitions/wiki_steps.rb
  63. 2
      features/step_definitions/work_package_changesets_steps.rb
  64. 2
      features/step_definitions/work_package_steps.rb
  65. 6
      features/support/env.rb
  66. 23
      frontend/app/angular-modules.ts
  67. 24
      frontend/app/angular4-modules.ts
  68. 3
      frontend/app/angular4-transition-utils.ts
  69. 3
      frontend/app/components/a11y/accessible-by-keyboard.component.ts
  70. 9
      frontend/app/components/a11y/accessible-by-keyboard.ng1.directive.ts
  71. 86
      frontend/app/components/a11y/accessible-click.directive.ng2.test.ts
  72. 33
      frontend/app/components/a11y/accessible-click.directive.ts
  73. 10
      frontend/app/components/a11y/accessible_by_keyboard.html
  74. 16
      frontend/app/components/angular/downgrade-attribute-directive.ts
  75. 36
      frontend/app/components/common/filters/html-escape.filter.ts
  76. 0
      frontend/app/components/common/focus/focus-directive-upgraded.ts
  77. 110
      frontend/app/components/common/focus/focus-helper.ts
  78. 0
      frontend/app/components/common/focus/focus-within.directive.ts
  79. 0
      frontend/app/components/common/focus/focus-within.upgraded.directive.ts
  80. 36
      frontend/app/components/common/focus/focus.directive.ts
  81. 2
      frontend/app/components/common/model-auth/model-auth.service.test.ts
  82. 2
      frontend/app/components/filters/wp-filters/wp-filters.service.test.ts
  83. 4
      frontend/app/components/filters/wp-filters/wp-filters.service.ts
  84. 7
      frontend/app/components/modals/export-modal/wp-table-export.modal.ts
  85. 2
      frontend/app/components/modals/rename-query-modal/rename-query.modal.ts
  86. 16
      frontend/app/components/modals/share-modal/query-sharing-form.component.ts
  87. 105
      frontend/app/components/modals/wp-destroy-modal/wp-destroy-modal.controller.ts
  88. 76
      frontend/app/components/modals/wp-destroy-modal/wp-destroy-modal.html
  89. 39
      frontend/app/components/modals/wp-destroy-modal/wp-destroy-modal.ts
  90. 78
      frontend/app/components/modals/wp-destroy-modal/wp-destroy.modal.html
  91. 118
      frontend/app/components/modals/wp-destroy-modal/wp-destroy.modal.ts
  92. 8
      frontend/app/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts
  93. 3
      frontend/app/components/op-context-menu/op-context-menu.html
  94. 5
      frontend/app/components/op-context-menu/op-context-menu.service.ts
  95. 11
      frontend/app/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts
  96. 8
      frontend/app/components/op-context-menu/wp-context-menu/wp-table-context-menu.directive.ts
  97. 5
      frontend/app/components/op-modals/op-modal.service.ts
  98. 3
      frontend/app/components/projects/current-project.service.test.ts
  99. 18
      frontend/app/components/routing/ui-router.config.ts
  100. 6
      frontend/app/components/routing/wp-full-view/wp-full-view.html
  101. Some files were not shown because too many files have changed in this diff Show More

@ -6,7 +6,7 @@
# 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-2017 Jean-Philippe Lang
# Copyright (C) 2006-2018 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
@ -23,9 +23,19 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
# See doc/COPYRIGHT.rdoc for more details.
#++
###################################
#
# This file was generated by
# openproject-devkit.
#
# Do not modify this file directly!
#
###################################
language: ruby
rvm:
@ -87,6 +97,7 @@ before_install:
- travis_retry gem update --system
- gem install bundler
bundler_args: --binstubs --without development production docker
before_script:
@ -95,17 +106,6 @@ before_script:
script:
- sh script/ci_runner.sh
notifications:
email: false
slack:
on_success: change
on_failure: always
on_pull_requests: false
rooms:
# CE
- secure: "YkbFN1VXIIKYqDkdH3MTyVyzf0D7wCmAApvMMtWwVX6XM8NZ8PocFd26Cb3NEVoNG9e4xo4XMOcSMhz8FhZ4GgQhV0+075yhYKt8nq12S9XabbuJq/pfmTKyTYk+5RK35TX5rJFsE54R5pjn+FA42b0N7/BsyOsoTLKv34Ei68bG1sV8m0Q5+wVcoB30be9BsUckKLG8OPglLG8ht13Sf5WZSwgMFrW8tlrexgh+U56LpkZDoVwidI/tgyMuBTHTiAa97M/YVzf5vl6Bsq9Y8VO6JVwN3Q8FYQydmp9nlaAIN79OfXtdjnKX79gqicUM3PzOUYASAFVfwbu1qx7Ugg8QUj9E9Z0fRu1yHK2u4qxwtQbraF6zlSzJUikvv7d5/EoW8SohfuACrs86GBUCaiQRa5q643qtI0W/7/bThGX28GHfgFTo1g/zttDXFa7iZuWjz6AsIrCzJBxL8LozzQHI94Z3H2/L6FwOU6ZZmWFSJ4dkODEdD9EbGUaHUzA5JmTvSopy5qIA7M2jFmv9NLPE0SjP4U352XTbMJ8MFaSvuJ7cGHPMBlYPWayk+RuL0PQuIIEQaFoxsae0moiCqzBVMDZv98N/BtLYIn8KkvleQbkqTdtO5lgMab5e4xJYD5frLnHmC7nPXh7c5ogbU0ILlF9L5XUcZz412PhDGYs="
# MP
- secure: "uhO7jqGYIL4QPPWwIzAKGvSuXI0E7n8HD9aoiYUO2nL9t0iW8TjpQ34ht1JhMGBiG8D86vjXXSWB+o0jEBACEYJ6GQhCTo2yuGaXoS3igBVs8Y2XD5M+nOF2OReeJ9RXOn9KvosXGfSxE8KX/6YuvzLKFV6CZL2/RKC2Euqr0MKMeiaGVaUgZxgYbReJsgfqHugZgOIQY7yeMvGoZXgDqj6oDwMyl993XVxoPcGRQV0laOAKN2+ZDo8LmE3xuKfEvmGLtYh+5zrsOTp3YRU7K/BgQlYrvUcYORj4XyzO2UAej/qRS5LqqrK5ZFwuwpH3NJYpFQNS4b0fq5IaA4dKz+N3TzNWp1oOWckE05/LWBWZYl1t8Xto0NIc2mMeSAzhV4CcAOc7W+fCNyCGxsRNpfDD2Tst02aQ+fi5pC3ITS7IskrpA86T+WVckDDjjsROGAUaYUTtMMDzWGcnP0815zoGW4cIGnjwcmCn8w9AE3D1ERUxAagIwLeXDtv2XBxphS58Pfqg1pmfywEVdIUs9DUbJ4WRH/oHnESLB0DG5i2BvHMplNBiBdxpKH7V3cYHyHFgl+ExjwfwLKZw75EyyjwfD7bmJmEUE9YpiXulzP/JRnUiQ5oorQ6fzVva4TTZ4jugc1xImiwpOv60sBSdEI+LnnVMQurhfC3HQSYl/wE="
addons:
chrome: stable

@ -153,9 +153,6 @@ gem 'sass', '3.5.1'
gem 'sass-rails', '~> 5.0.6'
gem 'sprockets', '~> 3.7.0'
# small wrapper around the command line
gem 'cocaine', '~> 0.6.0'
# required by Procfile, for deployment on heroku or packaging with packager.io.
# also, better than thin since we can control worker concurrency.
gem 'unicorn'
@ -168,8 +165,9 @@ gem 'nokogiri', '~> 1.8.2'
gem 'fog-aws'
gem 'carrierwave', '~> 1.2.2'
# Require aws-sdk for SMS and other features
gem 'aws-sdk', '~> 2.11.39'
gem 'aws-sdk-core', '~> 3.20.2'
# File upload via fog + screenshots on travis
gem 'aws-sdk-s3', '~> 1.9.1'
gem 'openproject-token', '~> 1.0.1'
@ -180,12 +178,12 @@ group :test do
gem 'shoulda-context', '~> 1.2'
gem 'launchy', '~> 2.4.3'
# Require factory_girl for usage with openproject plugins testing
# FactoryGirl needs to be available when loading app otherwise factory
# Require factory_bot for usage with openproject plugins testing
# FactoryBot needs to be available when loading app otherwise factory
# definitions from core are not available in the plugin thus specs break
gem 'factory_girl', '~> 4.5'
# require factory_girl_rails for convenience in core development
gem 'factory_girl_rails', '~> 4.7', require: false
gem 'factory_bot', '~> 4.8'
# require factory_bot_rails for convenience in core development
gem 'factory_bot_rails', '~> 4.8', require: false
# Test prof provides factories from code
# and other niceties

@ -137,13 +137,18 @@ GEM
execjs
awesome_nested_set (3.1.3)
activerecord (>= 4.0.0, < 5.2)
aws-sdk (2.11.39)
aws-sdk-resources (= 2.11.39)
aws-sdk-core (2.11.39)
aws-partitions (1.82.0)
aws-sdk-core (3.20.2)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.11.39)
aws-sdk-core (= 2.11.39)
aws-sdk-kms (1.5.0)
aws-sdk-core (~> 3)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.9.1)
aws-sdk-core (~> 3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
@ -188,9 +193,6 @@ GEM
chromedriver-helper (1.2.0)
archive-zip (~> 0.10)
nokogiri (~> 1.8)
climate_control (0.2.0)
cocaine (0.6.0)
terrapin (= 0.6.0)
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
@ -261,10 +263,10 @@ GEM
eventmachine (1.2.5)
excon (0.52.0)
execjs (2.7.0)
factory_girl (4.9.0)
factory_bot (4.8.2)
activesupport (>= 3.0.0)
factory_girl_rails (4.9.0)
factory_girl (~> 4.9.0)
factory_bot_rails (4.8.2)
factory_bot (~> 4.8.2)
railties (>= 3.0.0)
faker (1.8.4)
i18n (~> 0.5)
@ -568,8 +570,6 @@ GEM
svg-graph (2.1.3)
sys-filesystem (1.1.9)
ffi
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
test-prof (0.4.9)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
@ -631,7 +631,8 @@ DEPENDENCIES
airbrake (~> 5.1.0)
autoprefixer-rails (~> 7.1.5)
awesome_nested_set (~> 3.1.3)
aws-sdk (~> 2.11.39)
aws-sdk-core (~> 3.20.2)
aws-sdk-s3 (~> 1.9.1)
bcrypt (~> 3.1.6)
bootsnap (~> 1.1.2)
bourbon (~> 4.3.4)
@ -642,7 +643,6 @@ DEPENDENCIES
cells-erb (~> 0.0.8)
cells-rails (~> 0.0.6)
chromedriver-helper (~> 1.2.0)
cocaine (~> 0.6.0)
coderay (~> 1.1.2)
color-tools (~> 1.3.0)
commonmarker (~> 0.17.8)
@ -654,8 +654,8 @@ DEPENDENCIES
date_validator (~> 0.9.0)
delayed_job_active_record (~> 4.1.1)
equivalent-xml (~> 0.6)
factory_girl (~> 4.5)
factory_girl_rails (~> 4.7)
factory_bot (~> 4.8)
factory_bot_rails (~> 4.8)
faker
fog-aws
friendly_id (~> 5.2.1)

@ -29,3 +29,4 @@
//= require jstoolbar/jstoolbar
//= require jstoolbar/translations
//= require jstoolbar/textile
//= require jstoolbar/markdown

@ -0,0 +1,193 @@
/* ***** BEGIN LICENSE BLOCK *****
* This file is part of DotClear.
* Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
* rights reserved.
*
* DotClear 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.
*
* DotClear 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 DotClear; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* ***** END LICENSE BLOCK *****
*/
/* Modified by JP LANG for textile formatting */
// strong
jsToolBar.prototype.elements.strong = {
type: 'button',
title: 'Strong',
fn: {
wiki: function() { this.singleTag('**') }
}
};
// em
jsToolBar.prototype.elements.em = {
type: 'button',
title: 'Italic',
fn: {
wiki: function() { this.singleTag("_") }
}
};
// del
jsToolBar.prototype.elements.del = {
type: 'button',
title: 'Deleted',
fn: {
wiki: function() { this.singleTag('~~') }
}
};
// code
jsToolBar.prototype.elements.code = {
type: 'button',
title: 'Code',
fn: {
wiki: function() { this.singleTag('`') }
}
};
// spacer
jsToolBar.prototype.elements.space1 = {type: 'space'};
// headings
jsToolBar.prototype.elements.h1 = {
type: 'button',
title: 'Heading 1',
fn: {
wiki: function() {
this.encloseLineSelection('# ', '',function(str) {
str = str.replace(/^#+/, '');
return str;
});
}
}
};
jsToolBar.prototype.elements.h2 = {
type: 'button',
title: 'Heading 2',
fn: {
wiki: function() {
this.encloseLineSelection('## ', '',function(str) {
str = str.replace(/^#+/, '');
return str;
});
}
}
};
jsToolBar.prototype.elements.h3 = {
type: 'button',
title: 'Heading 3',
fn: {
wiki: function() {
this.encloseLineSelection('### ', '',function(str) {
str = str.replace(/^#+/, '');
return str;
});
}
}
};
// spacer
jsToolBar.prototype.elements.space2 = {type: 'space'};
// ul
jsToolBar.prototype.elements.ul = {
type: 'button',
title: 'Unordered list',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^)[#-]?\s*/g,"$1- ");
});
}
}
};
// ol
jsToolBar.prototype.elements.ol = {
type: 'button',
title: 'Ordered list',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^)[*-]?\s*/g,"$11. ");
});
}
}
};
// spacer
jsToolBar.prototype.elements.space3 = {type: 'space'};
// bq
jsToolBar.prototype.elements.bq = {
type: 'button',
title: 'Quote',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
});
}
}
};
// unbq
jsToolBar.prototype.elements.unbq = {
type: 'button',
title: 'Unquote',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
});
}
}
};
// pre
jsToolBar.prototype.elements.code = {
type: 'button',
title: 'Code fence',
fn: {
wiki: function() { this.encloseLineSelection('```\n', '\n```') }
}
};
// spacer
jsToolBar.prototype.elements.space4 = {type: 'space'};
// wiki page
jsToolBar.prototype.elements.link = {
type: 'button',
title: 'Link',
fn: {
wiki: function() { this.encloseSelection("[", "]()") }
}
};
// image
jsToolBar.prototype.elements.img = {
type: 'button',
title: 'Image',
fn: {
wiki: function() { this.encloseSelection("![](", ")") }
}
};

@ -59,7 +59,7 @@ jsToolBar.prototype.elements.del = {
};
// code
jsToolBar.prototype.elements.code = {
jsToolBar.prototype.elements.pre = {
type: 'button',
title: 'Code',
fn: {

@ -38,5 +38,5 @@ jQuery(document).ready(function () {
angular.element(document).ready(function() {
angular.bootstrap(document, ['openproject']);
});
angular.module('openproject-style-guide', ['ui.select', 'ngSanitize']);
angular.module('openproject-style-guide', ['ui.select']);

@ -161,7 +161,10 @@ $form--field-types: (text-field, text-area, select, check-box, radio-button, ran
border-bottom-width: 0px
.form--space
padding-top: 10px
padding-top: 1rem
&.-left-spacing
padding-left: 1rem
&.-big
padding-top: 20px

@ -64,7 +64,7 @@ $ng-modal-image-width: $ng-modal-image-height
overflow-x: hidden
&.-wide
width: 95%
min-width: 75vw
min-height: 40vh
.op-modal--modal-header

@ -69,6 +69,8 @@
text-decoration: none
&.selected
background-color: #fff
&.-disabled
color: #999
#content .tab-content
overflow-x: auto

@ -66,10 +66,13 @@
// Style hierarchy column differently
// Padding for the hierarchy mode
.wp-table--cell-td.subject:not(.-with-hierarchy)
padding-left: 25px !important
.wp-table--cell-td.subject
padding-left: 8px
.wp-table--cell-td.subject.-with-hierarchy
// Disable padding on the element itself
padding-left: 0
.wp-table--cell-span
padding-left: 0

@ -31,7 +31,8 @@
padding: 0
// Subject field
.work-package--single-view
.subject-header,
.wp-new--subject-wrapper
.wp-inline-edit--active-field.subject
.wp-inline-edit--field
height: 40px

@ -29,9 +29,15 @@
// Disable CSS containment in the embedded container
// unless we're setting an external height with overflow, containment will not work.
.work-packages-embedded-view--container
margin-left: -6px
.work-package-table--container
contain: initial !important
.generic-table--header,
.generic-table--sort-header
font-size: 12px
&.-compact-tables
.wp-table--row
@ -46,3 +52,16 @@
height: 28px !important
padding-top: 2px
padding-bottom: 2px
// Disable default padding when hierarchies are disabled
&.-hierarchy-disabled
.wp-table--cell-td.subject
padding-left: 0
// Reduce width of action column
.wp-table--context-menu-td,
.wp-table--context-menu-th
width: 25px
wp-query-group .wp-relations-create-button
margin-left: -6px

@ -33,7 +33,6 @@
container_class: '-wide'
)
%>
<%= wikitoolbar_for "settings-#{name}-#{lang}" %>
</div>
</div>
</div>

@ -135,7 +135,7 @@ class WikiController < ApplicationController
if @page.new_record?
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
edit
render action: 'edit'
render action: 'new'
else
render_404
end
@ -256,7 +256,7 @@ class WikiController < ApplicationController
def wiki_root_menu_items
MenuItems::WikiMenuItem
.where(parent_id: nil)
.main_items(@wiki.id)
.map { |it| OpenStruct.new name: it.name, caption: it.title, item: it }
end
@ -403,7 +403,7 @@ class WikiController < ApplicationController
private
def wiki_page_title
params[:id]
params[:title] || params[:id]
end
def find_wiki
@ -436,8 +436,7 @@ class WikiController < ApplicationController
# Returns the default content of a new wiki page
def initial_page_content(page)
helper = OpenProject::TextFormatting::Formatters.helper_for(Setting.text_formatting)
extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page)
helper.initial_page_content page
end
def load_pages_for_index

@ -32,6 +32,7 @@ module TextFormattingHelper
def_delegators :current_formatting_helper,
:text_formatting_has_preview?,
:text_formatting_js_includes,
:initial_page_content,
:wikitoolbar_for,
:heads_for_wiki_formatter
@ -55,8 +56,7 @@ module TextFormattingHelper
private
def current_formatting_helper
helper = OpenProject::TextFormatting::Formatters.helper_for(Setting.text_formatting)
extend helper
self
helper_class = OpenProject::TextFormatting::Formatters.helper_for(Setting.text_formatting)
helper_class.new(self)
end
end

@ -109,7 +109,10 @@ module ::TypesHelper
# Remove the templated filter since we can't yet handle it in the frontend
query.filters.delete_if(&:templated?)
::API::V3::Queries::QueryParamsRepresenter.new(query).to_h
# Modify the hash to match Rails array based +to_query+ transforms:
# e.g., { columns: [1,2] }.to_query == "columns[]=1&columns[]=2" (unescaped)
# The frontend will do that IFF the hash key is an array
::API::V3::Queries::QueryParamsRepresenter.new(query).to_json
end
def attr_form_map(key, represented)

@ -141,7 +141,7 @@ class MailHandler < ActionMailer::Base
# TODO: send a email to the user
logger.error e.message if logger
false
rescue MissingInformation
rescue MissingInformation => e
log "missing information from #{user}: #{e.message}", :error
false
rescue UnauthorizedAction

@ -31,6 +31,8 @@ module Queries
module Relations
module Filters
class FromFilter < ::Queries::Relations::Filters::RelationFilter
include ::Queries::Relations::Filters::VisibilityChecking
def type
:integer
end
@ -38,6 +40,12 @@ module Queries
def self.key
:from_id
end
private
def visibility_checked_sql(operator, values, visible_sql)
["from_id #{operator} (?) AND to_id IN (#{visible_sql})", values]
end
end
end
end

@ -36,6 +36,8 @@ module Queries
# Given relations [{ from_id: 3, to_id: 7 }, { from_id: 8, to_id: 3}]
# filtering by involved=3 would yield both these relations.
class InvolvedFilter < ::Queries::Relations::Filters::RelationFilter
include ::Queries::Relations::Filters::VisibilityChecking
def type
:integer
end
@ -44,15 +46,21 @@ module Queries
:involved
end
def where
integer_values = values.map(&:to_i)
private
def visibility_checked_sql(operator_string, values, visible_sql)
concatenation = if operator == '='
"OR"
else
"AND"
end
sql = <<-SQL.strip_heredoc
(from_id #{operator_string} (?) AND to_id IN (#{visible_sql}))
#{concatenation} (to_id #{operator_string} (?) AND from_id IN (#{visible_sql}))
SQL
case operator
when "="
["from_id IN (?) OR to_id IN (?)", integer_values, integer_values]
when "!"
["from_id NOT IN (?) AND to_id NOT IN (?)", integer_values, integer_values]
end
[sql, values, values]
end
end
end

@ -36,6 +36,10 @@ module Queries
def human_name
Relation.human_attribute_name(name)
end
def visibility_checked?
false
end
end
end
end

@ -31,6 +31,8 @@ module Queries
module Relations
module Filters
class ToFilter < ::Queries::Relations::Filters::RelationFilter
include ::Queries::Relations::Filters::VisibilityChecking
def type
:integer
end
@ -38,6 +40,16 @@ module Queries
def self.key
:to_id
end
def visibility_checked?
true
end
private
def visibility_checked_sql(operator, values, visible_sql)
["to_id #{operator} (?) AND from_id IN (#{visible_sql})", values]
end
end
end
end

@ -0,0 +1,62 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
module Queries
module Relations
module Filters
module VisibilityChecking
def visibility_checked?
true
end
def where
integer_values = values.map(&:to_i)
visible_sql = WorkPackage.visible(User.current).select(:id).to_sql
operator_string = case operator
when "="
"IN"
when "!"
"NOT IN"
end
visibility_checked_sql(operator_string, values, visible_sql)
end
private
def visibility_checked_sql(_operator, _values, _visible_sql)
raise NotImplementedError
end
end
end
end
end

@ -35,9 +35,19 @@ module Queries
def default_scope
Relation
.visible
.direct
end
def results
# Filters marked to already check visibility free us from the need
# to check it here.
if filters.any?(&:visibility_checked?)
super
else
super.visible
end
end
end
end
end

@ -126,7 +126,7 @@ class TimeEntry < ActiveRecord::Base
private
def validate_hours_are_in_range
errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
errors.add :hours, :invalid if hours && hours < 0
end
def validate_project_is_set

@ -33,6 +33,7 @@ module Type::AttributeGroups
included do
before_save :write_attribute_groups_objects
after_save :unset_attribute_groups_objects
after_destroy :remove_attribute_groups_queries
validate :validate_attribute_group_names
validate :validate_attribute_groups
@ -96,10 +97,19 @@ module Type::AttributeGroups
attribute_groups_objects
end
def attribute_groups=(groups)
new_groups = groups.empty? ? default_attribute_groups : groups
##
# Resets the default attribute groups
def reset_attribute_groups
# Remove all active custom fields
self.custom_field_ids = []
self.attribute_groups_objects = to_attribute_group_class(default_attribute_groups)
end
self.attribute_groups_objects = to_attribute_group_class(new_groups)
##
# Update the attribute groups object.
def attribute_groups=(groups)
self.attribute_groups_objects = to_attribute_group_class(groups)
end
##
@ -113,16 +123,18 @@ module Type::AttributeGroups
array << [groupkey, members] if members.present?
end
groups << [:children, [default_children_query]]
groups
end
def reload(*args)
self.attribute_groups_objects = nil
unset_attribute_groups_objects
super
end
def unset_attribute_groups_objects
self.attribute_groups_objects = nil
end
protected
attr_accessor :attribute_groups_objects
@ -194,10 +206,16 @@ module Type::AttributeGroups
end
end
##
# Get the default attribute groups for this type.
# If it has activated custom fields through +custom_field_ids=+,
# it will put them into the other group.
def work_package_attributes_by_default_group_key
active_cfs = active_custom_field_attributes
work_package_attributes
.keys
.reject { |key| CustomField.custom_field_attribute?(key) && !has_custom_field?(key) }
.reject { |key| CustomField.custom_field_attribute?(key) && !active_cfs.include?(key) }
.group_by { |key| default_group_key(key.to_sym) }
end
@ -233,17 +251,6 @@ module Type::AttributeGroups
end
end
def default_children_query
query = Query.new_default
query.name = 'children'
query.is_public = false
query.column_names = %w(id type subject)
query.show_hierarchies = false
query.filters = []
query.add_filter('parent', '=', ::Queries::Filters::TemplatedValue::KEY)
query
end
def new_attribute_group(key, attributes)
Type::AttributeGroup.new(self, key, attributes)
end

@ -175,12 +175,9 @@ module Type::Attributes
end
##
# Returns whether this type has the custom field currently
# (e.g. because it was checked in the removed CF view).
def has_custom_field?(attribute)
@has_custom_field_custom_field_ids ||= custom_field_ids.map { |id| "custom_field_#{id}" }
@has_custom_field_custom_field_ids.include? attribute
# Returns the active custom_field_attributes
def active_custom_field_attributes
custom_field_ids.map { |id| "custom_field_#{id}" }
end
##

@ -188,6 +188,31 @@ class WorkPackage < ActiveRecord::Base
Relation.of_work_package(self)
end
def visible_relations(user)
# This duplicates chaining
# .relations.visible
# The duplication is made necessary to achive a performant sql query on MySQL.
# Chaining would result in
# WHERE (relations.from_id = [ID] OR relations.to_id = [ID])
# AND relations.from_id IN (SELECT [IDs OF VISIBLE WORK_PACKAGES])
# AND relations.to_id IN (SELECT [IDs OF VISIBLE WORK_PACKAGES])
# This performs OK on postgresql but is very slow on MySQL
# The SQL generated by this method:
# WHERE (relations.from_id = [ID] AND relations.to_id IN (SELECT [IDs OF VISIBLE WORK_PACKAGES])
# OR (relations.to_id = [ID] AND relations.from_id IN (SELECT [IDs OF VISIBLE WORK_PACKAGES]))
# is arguably easier to read and performs equally good on both DBs.
relations_from = Relation
.where(from: self)
.where(to: WorkPackage.visible(user))
relations_to = Relation
.where(to: self)
.where(from: WorkPackage.visible(user))
relations_from
.or(relations_to)
end
def relation(id)
Relation.of_work_package(self).find(id)
end

@ -36,6 +36,7 @@ class QueryPolicy < BasePolicy
update: persisted_and_own_or_public?(cached_query),
destroy: persisted_and_own_or_public?(cached_query),
create: create_allowed?(cached_query),
create_new: create_new_allowed?(cached_query),
publicize: publicize_allowed?(cached_query),
depublicize: depublicize_allowed?(cached_query),
star: persisted_and_own_or_public?(cached_query),
@ -58,8 +59,11 @@ class QueryPolicy < BasePolicy
end
def create_allowed?(query)
query.new_record? &&
save_queries_allowed?(query)
query.new_record? && create_new_allowed?(query)
end
def create_new_allowed?(query)
save_queries_allowed?(query)
end
def publicize_allowed?(query)

@ -46,14 +46,14 @@ module DevelopmentData
def create_types!(cfs)
# Create ALL CFs types
non_req_cfs = cfs.reject(&:is_required).map { |cf| "custom_field_#{cf.id}" }
type = FactoryGirl.build :type_with_workflow, name: 'All CFS'
type = FactoryBot.build :type_with_workflow, name: 'All CFS'
extend_group(type, ['Custom fields', non_req_cfs])
type.save!
print '.'
# Create type
req_cfs = cfs.select(&:is_required).map { |cf| "custom_field_#{cf.id}" }
type_req = FactoryGirl.build :type_with_workflow, name: 'Required CF'
type_req = FactoryBot.build :type_with_workflow, name: 'Required CF'
extend_group(type_req, ['Custom fields', req_cfs])
type_req.save!
print '.'

@ -63,9 +63,9 @@ class RootSeeder < Seeder
if Rails.env.development?
puts '*** Seeding development data'
require 'factory_girl'
# Load FactoryGirl factories
::FactoryGirl.find_definitions
require 'factory_bot'
# Load FactoryBot factories
::FactoryBot.find_definitions
DevelopmentDataSeeder.new.seed!
end

@ -113,7 +113,7 @@ module API
end
def columns_from_params(params)
columns = params[:columns] || params[:c] || params[:column_names]
columns = params[:columns] || params[:c] || params[:column_names] || params[:'columns[]']
return unless columns

@ -68,8 +68,11 @@ class BaseTypeService
end
def set_attribute_groups(params)
groups = parse_attribute_groups_params(params)
type.attribute_groups = groups if groups
if params[:attribute_groups].present?
type.attribute_groups = parse_attribute_groups_params(params)
else
type.reset_attribute_groups
end
end
def parse_attribute_groups_params(params)
@ -83,17 +86,17 @@ class BaseTypeService
end
def transform_query_params_to_query(groups)
groups.each_with_index do |(_name, attributes), index|
groups.each_with_index do |(name, attributes), index|
next unless attributes.is_a? Hash
next if attributes.values.compact.empty?
# HACK: have sensible name (although it should never be visible)
call = ::API::V3::UpdateQueryFromV3ParamsService
.new(Query.new_default(name: 'some_name'), user)
.new(Query.new_default(name: "Embedded subelements: #{name}"), user)
.call(attributes.with_indifferent_access)
query = call.result
query.show_hierarchies = false
query.add_filter('parent', '=', ::Queries::Filters::TemplatedValue::KEY)
groups[index][1] = [query]

@ -89,7 +89,7 @@ See docs/COPYRIGHT.rdoc for more details.
<td><%= I18n.t('my_account.access_tokens.indefinite_expiration') %></td>
<td>
<%= link_to l(:button_reset),
{ action: 'generate_api_key' },
{ action: 'generate_rss_key' },
method: :post,
class: 'icon icon-delete' %>
</td>

@ -29,9 +29,9 @@ See docs/COPYRIGHT.rdoc for more details.
<modal-wrapper iframe-url="<%= OpenProject::Configuration.onboarding_video_url %>"
modal-class-name="onboarding-modal -highlight">
<li>
<%= link_to l(:label_video),
<%= link_to l(:label_introduction_video),
'',
title: l(:label_video),
title: l(:label_introduction_video),
class: 'modal-wrapper--activation-link' %>
</li>
<%= render partial: '/onboarding/starting_video_modal' %>

@ -29,8 +29,9 @@ See docs/COPYRIGHT.rdoc for more details.
<div class="form--field">
<%= form.text_area :description,
id: :project_description,
with_text_formatting: true,
rows: 5,
class: 'wiki-edit',
container_class: '-wide' %>
</div>
<%= wikitoolbar_for 'project_description' %>

@ -50,6 +50,12 @@ See docs/COPYRIGHT.rdoc for more details.
</div>
<div class="form--field"><%= setting_select :protocol, [['HTTP', 'http'], ['HTTPS', 'https']], container_class: '-xslim' %></div>
<div class="form--field"><%= setting_select :text_formatting, OpenProject::TextFormatting::Formatters.format_names.collect{|name| [t("text_formatting.#{name}"), name]}, container_class: '-xslim' %></div>
<div class="form--field">
<%= setting_check_box :use_wysiwyg %>
<span class="form--field-instructions">
<%= t(:setting_use_wysiwyg_description) %>
</span>
</div>
<div class="form--field"><%= setting_check_box :cache_formatted_text %></div>
<div class="form--field"><%= setting_select :wiki_compression, [['Gzip', 'gzip']], blank: :label_none, container_class: '-xslim' %></div>
<div class="form--field"><%= setting_check_box :feeds_enabled, size: 6 %></div>
@ -63,8 +69,13 @@ See docs/COPYRIGHT.rdoc for more details.
<legend class="form--fieldset-legend"><%= t(:setting_welcome_text) %></legend>
<div class="form--field"><%= setting_text_field :welcome_title, size: 30, container_class: '-wide' %></div>
<div class="form--field">
<%= setting_text_area :welcome_text, cols: 60, rows: 5, class: 'wiki-edit', container_class: '-wide' %>
<%= wikitoolbar_for 'settings_welcome_text' %>
<%= setting_text_area :welcome_text,
cols: 60,
rows: 5,
class: 'wiki-edit',
id: 'settings_welcome_text',
with_text_formatting: true,
container_class: '-wide' %>
</div>
<div class="form--field"><%= setting_check_box :welcome_on_homescreen %></div>
<div class="form--field"><%= setting_check_box :welcome_on_projects_page %></div>

@ -147,7 +147,7 @@ See docs/COPYRIGHT.rdoc for more details.
<div id="draggable-groups" dragula='"groups"'>
<% form_attributes[:actives].each do |group, attributes| %>
<% if group.is_a? ::Type::QueryGroup %>
<%= render partial: 'types/form/query_group', locals: { group: group, query: attributes } %>
<%= render partial: 'types/form/query_group', locals: { group: group, query_json: attributes } %>
<% else %>
<%= render partial: 'types/form/attribute_group', locals: { group: group, attributes: attributes } %>
<% end %>

@ -16,7 +16,7 @@
</div>
<%= content_tag :div,
class: 'type-form-query',
data: { query: JSON.dump(query) } do %>
data: { query: query_json } do %>
<span class="type-form-query-group--edit-button" ng-click="editQuery($event)">
<%= op_icon('button--icon icon-edit') %>
<%= t('types.edit.edit_query') %>

@ -1227,6 +1227,7 @@ en:
label_information_plural: "Information"
label_integer: "Integer"
label_internal: "Internal"
label_introduction_video: "Introduction video"
label_invite_user: "Invite user"
label_show_hide: "Show/hide"
label_show_all_registered_users: "Show all registered users"
@ -1482,7 +1483,7 @@ en:
label_version_sharing_none: "Not shared"
label_version_sharing_system: "With all projects"
label_version_sharing_tree: "With project tree"
label_video: "Video"
label_videos: "Videos"
label_view_all_revisions: "View all revisions"
label_view_diff: "View differences"
label_view_revisions: "View revisions"
@ -1944,6 +1945,8 @@ en:
setting_brute_force_block_after_failed_logins: "Block user after this number of failed login attempts"
setting_brute_force_block_minutes: "Time the user is blocked for"
setting_cache_formatted_text: "Cache formatted text"
setting_use_wysiwyg: "WYSIWYG editor"
setting_use_wysiwyg_description: "Select to enable CKEditor5 WYSIWYG editor for all users by default. CKEditor has limited functionality for GFM Markdown."
setting_column_options: "Customize the appearance of the work package lists"
setting_commit_fix_keywords: "Fixing keywords"
setting_commit_logs_encoding: "Commit messages encoding"

@ -304,6 +304,7 @@ en:
relations_hierarchy:
parent_headline: "Parent"
children_headline: "Children"
relation_buttons:
change_parent: "Change parent"
@ -565,6 +566,7 @@ en:
table_configuration:
button: 'Configure this work package table'
modal_title: 'Work package table configuration'
embedded_tab_disabled: "This configuration tab is not available for the embedded query you're editing."
display_settings: 'Display settings'
grouped_mode: "Grouped mode"
grouped_hint: "Table results will be grouped by the given attribute."

@ -131,6 +131,8 @@ plain_text_mail:
default: 0
text_formatting:
default: textile
use_wysiwyg:
default: 1
cache_formatted_text:
default: 0
wiki_compression:

@ -337,4 +337,4 @@ default:
## Onboarding variables:
* 'onboarding_video_url': An URL for the video displayed on the onboarding modal. This is only shown when the user logs in for the first time.
* `onboarding_video_url`: An URL for the video displayed on the onboarding modal. This is only shown when the user logs in for the first time.

@ -28,7 +28,7 @@
#++
Given(/^there is a board "(.*?)" for project "(.*?)"$/) do |board_name, project_identifier|
FactoryGirl.create :board, project: get_project(project_identifier), name: board_name
FactoryBot.create :board, project: get_project(project_identifier), name: board_name
end
Given(/^the board "(.*?)" has the following messages:$/) do |board_name, table|
@ -47,7 +47,7 @@ private
def create_messages(names, board, parent = nil)
names.each do |name|
FactoryGirl.create :message,
FactoryBot.create :message,
board: board,
subject: name,
parent: parent

@ -44,7 +44,7 @@ Given /^the [pP]roject(?: "([^\"]+?)")? uses the following types:$/ do |project,
name = line.first
type = ::Type.find_by(name: name)
type = FactoryGirl.create(:type, name: name) if type.blank?
type = FactoryBot.create(:type, name: name) if type.blank?
type
}

@ -48,7 +48,7 @@ Given /^the following (user|issue|work package) custom fields are defined:$/ do
attr_hash[:default_value] = r[:default_value] ? r[:default_value] : nil
attr_hash[:is_for_all] = r[:is_for_all] || true
FactoryGirl.create type, attr_hash
FactoryBot.create type, attr_hash
end
end
end

@ -32,14 +32,14 @@ require 'rack_session_access/capybara'
Before do |scenario|
unless ScenarioDisabler.empty_if_disabled(scenario)
FactoryGirl.create(:admin) unless User.find_by_login('admin')
FactoryGirl.create(:anonymous) unless AnonymousUser.count > 0
FactoryBot.create(:admin) unless User.find_by_login('admin')
FactoryBot.create(:anonymous) unless AnonymousUser.count > 0
Setting.notified_events = [] # can not test mailer
end
end
Given /^I am logged in$/ do
@user = FactoryGirl.create :user
@user = FactoryBot.create :user
page.set_rack_session(user_id: @user.id, updated_at: Time.now)
end
@ -86,7 +86,7 @@ Given /^(?:|I )am (not )?impaired$/ do |bool|
end
Given /^there is 1 [pP]roject with(?: the following)?:$/ do |table|
p = FactoryGirl.build(:project)
p = FactoryBot.build(:project)
send_table_to_object(p, table)
end
@ -121,8 +121,8 @@ Given /^the [Uu]ser "([^\"]*)" has 1 time [eE]ntry$/ do |user|
u = User.find_by login: user
p = u.projects.last
raise 'This user must be member of a project to have issues' unless p
i = FactoryGirl.create(:work_package, project: p)
t = FactoryGirl.build(:time_entry)
i = FactoryBot.create(:work_package, project: p)
t = FactoryBot.build(:time_entry)
t.user = u
t.issue = i
t.project = p
@ -134,8 +134,8 @@ end
Given /^the [Uu]ser "([^\"]*)" has 1 time entry with (\d+\.?\d*) hours? at the project "([^\"]*)"$/ do |user, hours, project|
p = Project.find_by(name: project) || Project.find_by(identifier: project)
as_admin do
t = FactoryGirl.build(:time_entry)
i = FactoryGirl.create(:work_package, project: p)
t = FactoryBot.build(:time_entry)
i = FactoryBot.create(:work_package, project: p)
t.project = p
t.issue = i
t.hours = hours.to_f
@ -149,8 +149,8 @@ end
Given /^the [Pp]roject "([^\"]*)" has (\d+) [tT]ime(?: )?[eE]ntr(?:ies|y) with the following:$/ do |project, count, table|
p = Project.find_by(name: project) || Project.find_by(identifier: project)
as_admin count do
t = FactoryGirl.build(:time_entry)
i = FactoryGirl.create(:work_package, project: p)
t = FactoryBot.build(:time_entry)
i = FactoryBot.create(:work_package, project: p)
t.project = p
t.work_package = i
t.activity.project = p
@ -173,14 +173,14 @@ end
Given /^the [pP]roject "([^\"]*)" has 1 [sS]ubproject$/ do |project|
parent = Project.find_by(name: project)
p = FactoryGirl.create(:project)
p = FactoryBot.create(:project)
p.set_parent!(parent)
p.save!
end
Given /^the [pP]roject "([^\"]*)" has 1 [sS]ubproject with the following:$/ do |project, table|
parent = Project.find_by(name: project)
p = FactoryGirl.build(:project)
p = FactoryBot.build(:project)
as_admin do
send_table_to_object(p, table)
end
@ -231,7 +231,7 @@ Given /^the [iI]ssue "([^\"]*)" has (\d+) [tT]ime(?: )?[eE]ntr(?:ies|y) with the
i = WorkPackage.where(["subject = '#{issue}'"]).last
raise "No such issue: #{issue}" unless i
as_admin count do
t = FactoryGirl.build(:time_entry)
t = FactoryBot.build(:time_entry)
t.project = i.project
t.spent_on = DateTime.now
t.work_package = i

@ -28,7 +28,7 @@
#++
Given /^there is 1 group with the following:$/ do |table|
group = FactoryGirl.build(:group)
group = FactoryBot.build(:group)
send_table_to_object group, table, name: Proc.new { |group, name| group.lastname = name }
end
@ -63,11 +63,11 @@ When /^I add the user "(.+)" to the group$/ do |user_login|
end
Given /^We have the group "(.*?)"/ do |name|
group = FactoryGirl.create(:group, lastname: name)
group = FactoryBot.create(:group, lastname: name)
end
Given /^there is a group named "(.*?)" with the following members:$/ do |name, table|
group = FactoryGirl.create(:group, lastname: name)
group = FactoryBot.create(:group, lastname: name)
table.raw.flatten.each do |login|
group.users << User.find_by!(login: login)

@ -33,7 +33,7 @@ Given /^the [Pp]roject "([^\"]*)" has (\d+) [cC]ategor(?:ies|y)? with(?: the fol
p = Project.find_by(name: project) || Project.find_by(identifier: project)
table.rows_hash['assigned_to'] = Principal.like(table.rows_hash['assigned_to']).first if table.rows_hash['assigned_to']
as_admin count do
ic = FactoryGirl.build(:category, project: p)
ic = FactoryBot.build(:category, project: p)
send_table_to_object(ic, table)
ic.save
end
@ -42,7 +42,7 @@ end
Given /^the [Pp]roject "([^\"]*)" has (\d+) [cC]ategor(?:ies|y)?$/ do |project, count|
p = Project.find_by(name: project) || Project.find_by(identifier: project)
as_admin count do
ic = FactoryGirl.build(:category, project: p)
ic = FactoryBot.build(:category, project: p)
ic.save
end
end

@ -46,7 +46,7 @@ Given(/^the issue "(.*?)" has an attachment "(.*?)"$/) do |issue_subject, file_n
issue = WorkPackage.where(subject: issue_subject).order(:created_at).last
file = OpenProject::Files.create_temp_file name: file_name,
content: 'random content which is not actually a gif'
attachment = FactoryGirl.create :attachment,
attachment = FactoryBot.create :attachment,
author: issue.author,
content_type: content_type,
file: file,
@ -60,11 +60,11 @@ Given /^the [Uu]ser "([^\"]*)" has (\d+) [iI]ssue(?:s)? with(?: the following)?:
u = User.find_by login: user
raise 'This user must be member of a project to have issues' unless u.projects.last
as_admin count do
i = FactoryGirl.create(:work_package,
i = FactoryBot.create(:work_package,
project: u.projects.last,
author: u,
assigned_to: u,
status: Status.default || FactoryGirl.create(:status))
status: Status.default || FactoryBot.create(:status))
i.type = ::Type.find_by(name: table.rows_hash.delete('type')) if table.rows_hash['type']
@ -76,7 +76,7 @@ end
Given /^the [Pp]roject "([^\"]*)" has (\d+) [iI]ssue(?:s)? with(?: the following)?:$/ do |project, count, table|
p = Project.find_by(name: project) || Project.find_by(identifier: project)
as_admin count do
i = FactoryGirl.build(:work_package, project: p,
i = FactoryBot.build(:work_package, project: p,
type: p.types.first)
send_table_to_object(i, table, {}, method(:add_custom_value_to_issue))
end
@ -121,7 +121,7 @@ Given (/^there are the following issues with attributes:$/) do |table|
category = Category.find_by(name: attributes.delete('category'))
attributes.merge! category_id: category.id if category
issue = FactoryGirl.create(:work_package, attributes)
issue = FactoryBot.create(:work_package, attributes)
if watchers
watchers.split(',').each do |w| issue.add_watcher User.find_by_login(w) end

@ -32,7 +32,7 @@ InstanceFinder.register(IssuePriority, Proc.new { |name| IssuePriority.find_by(n
Given /^there is a(?:n)? (default )?issuepriority with:$/ do |default, table|
name = table.raw.find { |ary| ary.include? 'name' }[table.raw.first.index('name') + 1].to_s
project = get_project
FactoryGirl.build(:priority).tap do |prio|
FactoryBot.build(:priority).tap do |prio|
prio.name = name
prio.is_default = !!default
prio.project = project
@ -43,7 +43,7 @@ Given /^there are the following priorities:$/ do |table|
table.hashes.each do |row|
project = get_project
FactoryGirl.build(:priority).tap do |prio|
FactoryBot.build(:priority).tap do |prio|
prio.name = row[:name]
prio.is_default = row[:default] == 'true'
prio.project = project

@ -35,5 +35,5 @@ Given /^there is a project named "([^"]*)"(?: of type "([^"]*)")?$/ do |name, pr
attributes.merge!(project_type: ProjectType.find_by!(name: project_type_name))
end
FactoryGirl.create(:project, attributes)
FactoryBot.create(:project, attributes)
end

@ -30,7 +30,7 @@
Given /^the [Pp]roject "([^\"]*)" has (\d+) [wW]ork [pP]ackage [qQ]uer(?:ies|y)? with(?: the following)?:$/ do |project, count, table|
p = Project.find_by(name: project) || Project.find_by(identifier: project)
as_admin count do
i = FactoryGirl.build(:query, project: p)
i = FactoryBot.build(:query, project: p)
send_table_to_object(i, table)
i.save
end
@ -39,7 +39,7 @@ end
Given /^the [Pp]roject "([^\"]*)" has (\d+) [wW]ork [pP]ackage [qQ]uer(?:ies|y)?$/ do |project, count|
p = Project.find_by(name: project) || Project.find_by(identifier: project)
as_admin count do
i = FactoryGirl.build(:query, project: p)
i = FactoryBot.build(:query, project: p)
i.save
end
end

@ -30,7 +30,7 @@
Given(/^the project "(.*?)" has a repository$/) do |project_name|
project = Project.find(project_name)
repo = FactoryGirl.build(:repository_subversion,
repo = FactoryBot.build(:repository_subversion,
project: project)
Setting.enabled_scm = Setting.enabled_scm << repo.vendor

@ -41,16 +41,16 @@ Given /^the [Uu]ser "([^\"]*)" is a "([^\"]*)" (?:in|of) the [Pp]roject "([^\"]*
end
Given /^there is a [rR]ole "([^\"]*)"$/ do |name, _table = nil|
FactoryGirl.create(:role, name: name) unless Role.find_by(name: name)
FactoryBot.create(:role, name: name) unless Role.find_by(name: name)
end
Given /^there is a [rR]ole "([^\"]*)" with the following permissions:?$/ do |name, table|
FactoryGirl.create(:role, name: name, permissions: table.raw.flatten) unless Role.find_by(name: name)
FactoryBot.create(:role, name: name, permissions: table.raw.flatten) unless Role.find_by(name: name)
end
Given /^there are the following roles:$/ do |table|
table.raw.flatten.each do |name|
FactoryGirl.create(:role, name: name) unless Role.find_by(name: name)
FactoryBot.create(:role, name: name) unless Role.find_by(name: name)
end
end

@ -28,7 +28,7 @@
#++
Given(/^there are (\d+) work packages with "(.*?)" in their description$/) do |num, desc|
work_packages = FactoryGirl.create_list :work_package, num.to_i, description: desc
work_packages = FactoryBot.create_list :work_package, num.to_i, description: desc
time = Time.now
# ensure temporal order:
work_packages.reverse.each_with_index do |wp, i|

@ -37,7 +37,7 @@ Given /^there are the following status:$/ do |table|
attributes = row.inject({}) { |mem, (k, v)| mem[k.to_sym] = v if v.present?; mem }
attributes[:is_default] = attributes.delete(:default) == 'true'
FactoryGirl.create(:status, attributes)
FactoryBot.create(:status, attributes)
end
end

@ -29,11 +29,11 @@
Given(/^there is a time entry for "(.*?)" with (\d+) hours$/) do |subject, hours|
work_package = WorkPackage.find_by(subject: subject)
time_entry = FactoryGirl.create(:time_entry, work_package: work_package, hours: hours, project: work_package.project)
time_entry = FactoryBot.create(:time_entry, work_package: work_package, hours: hours, project: work_package.project)
end
Given(/^there is an activity "(.*?)"$/) do |name|
FactoryGirl.create(:time_entry_activity, name: name)
FactoryBot.create(:time_entry_activity, name: name)
end
When(/^I log (\d+) hours with the comment "(.*?)"$/) do |hours, comment|

@ -35,7 +35,7 @@ RouteMap.register(::Type, '/types')
Given /^the following types are enabled for the project called "(.*?)":$/ do |project_name, type_name_table|
types = type_name_table.raw.flatten.map { |type_name|
::Type.find_by(name: type_name) || FactoryGirl.create(:type, name: type_name)
::Type.find_by(name: type_name) || FactoryBot.create(:type, name: type_name)
}
project = Project.find_by(identifier: project_name)

@ -73,7 +73,7 @@ Given /^there is 1 [Uu]ser with(?: the following)?:$/ do |table|
user = User.find_by_login(login) unless login.blank?
if !user
user = FactoryGirl.create(:user)
user = FactoryBot.create(:user)
user.pref
user.password = user.password_confirmation = nil
end
@ -110,7 +110,7 @@ end
Given /^there are the following users:$/ do |table|
table.raw.flatten.each do |login|
FactoryGirl.create(:user, login: login)
FactoryBot.create(:user, login: login)
end
end

@ -34,7 +34,7 @@ Given /^the [Pp]roject (.+) has 1 version with(?: the following)?:$/ do |project
p = Project.find_by(name: project) || Project.find_by(identifier: project)
as_admin do
v = FactoryGirl.build(:version) { |v|
v = FactoryBot.build(:version) { |v|
v.project = p
}
send_table_to_object(v, table)

@ -32,8 +32,8 @@ Given /^the [Pp]roject "([^\"]*)" has 1 [wW]iki(?: )?[pP]age with the following:
p.wiki = Wiki.create unless p.wiki
page = FactoryGirl.create(:wiki_page, wiki: p.wiki)
content = FactoryGirl.create(:wiki_content, page: page)
page = FactoryBot.create(:wiki_page, wiki: p.wiki)
content = FactoryBot.create(:wiki_content, page: page)
send_table_to_object(page, table)
end
@ -43,7 +43,7 @@ Given /^there are no wiki menu items$/ do
end
Given /^the project "(.*?)" has (?:1|a) wiki menu item with the following:$/ do |project_name, table|
item = FactoryGirl.build(:wiki_menu_item)
item = FactoryBot.build(:wiki_menu_item)
send_table_to_object(item, table)
item.wiki = Project.find_by(name: project_name).wiki
item.save!
@ -51,11 +51,11 @@ end
Given /^the project "(.*?)" has a child wiki page of "(.*?)" with the following:$/ do |project_name, parent_page_title, table|
wiki = Project.find_by(name: project_name).wiki
wikipage = FactoryGirl.build(:wiki_page, wiki: wiki)
wikipage = FactoryBot.build(:wiki_page, wiki: wiki)
send_table_to_object(wikipage, table)
FactoryGirl.create(:wiki_content, page: wikipage)
FactoryBot.create(:wiki_content, page: wikipage)
parent_page = WikiPage.find_by(wiki_id: wiki.id, title: parent_page_title)
wikipage.parent_id = parent_page.id
@ -87,15 +87,15 @@ Given /^the wiki page "([^"]*)" of the project "([^"]*)" has (\d+) versions{0,1}
wiki = project.wiki
wp = wiki.pages.find_or_create_by(title: page)
wp.save! unless wp.persisted?
wc = wp.content || FactoryGirl.create(:wiki_content, page: wp)
wc = wp.content || FactoryBot.create(:wiki_content, page: wp)
last_version = wc.journals.max(&:version).version
version_count.to_i.times.each do |v|
version = last_version + v + 1
data = FactoryGirl.build(:journal_wiki_content_journal,
data = FactoryBot.build(:journal_wiki_content_journal,
text: "This is version #{version}")
FactoryGirl.create(:wiki_content_journal,
FactoryBot.create(:wiki_content_journal,
version: version,
data: data,
journable_id: wc.id)

@ -33,7 +33,7 @@ Given(/^the work package "(.*?)" has the following changesets:$/) do |subject, t
repo = wp.project.repository
wp_changesets = table.hashes.map { |row|
FactoryGirl.build(:changeset, row.merge(repository: repo))
FactoryBot.build(:changeset, row.merge(repository: repo))
}
wp.changesets = wp_changesets

@ -48,7 +48,7 @@ Given /^a relation between "(.*?)" and "(.*?)"$/ do |work_package_from, work_pac
from = WorkPackage.find_by(subject: work_package_from)
to = WorkPackage.find_by(subject: work_package_to)
FactoryGirl.create :relation, from: from, to: to
FactoryBot.create :relation, from: from, to: to
end
Given /^user is already watching "(.*?)"$/ do |work_package_subject|

@ -42,7 +42,7 @@ end
require 'cucumber/rails'
require 'cucumber/rspec/doubles'
require 'capybara-screenshot/cucumber'
require 'factory_girl_rails'
require 'factory_bot_rails'
# Load paths to ensure they are loaded before the plugin's paths.rbs.
# Plugin's path_to functions rely on being loaded after the core's path_to
@ -150,8 +150,8 @@ Before do
end
Before do
FactoryGirl.create(:non_member)
FactoryGirl.create(:anonymous_role)
FactoryBot.create(:non_member)
FactoryBot.create(:anonymous_role)
end
World(Capybara::Select2)

@ -44,20 +44,10 @@ export const opServicesModule = angular.module('openproject.services', [
'openproject.helpers',
'openproject.workPackages.config',
'openproject.workPackages.helpers',
'openproject.api',
'angular-cache',
'openproject.filters'
'openproject.api'
]);
angular.module('openproject.helpers', ['openproject.services']);
export const opModelsModule = angular.module('openproject.models', [
'openproject.workPackages.config',
'openproject.services'
]);
export const opViewModelsModule = angular.module('openproject.viewModels', [
'openproject.services'
]);
// work packages
export const opWorkPackagesModule = angular.module('openproject.workPackages', [
'openproject.workPackages.activities',
@ -81,13 +71,10 @@ angular.module('openproject.workPackages.filters', [
]);
angular.module('openproject.workPackages.config', []);
export const wpControllersModule = angular.module('openproject.workPackages.controllers', [
'openproject.models',
'openproject.viewModels',
'openproject.workPackages.helpers',
'openproject.services',
'openproject.workPackages.config',
'openproject.layout',
'btford.modal'
'openproject.layout'
]);
angular.module('openproject.workPackages.models', []);
export const wpDirectivesModule = angular.module('openproject.workPackages.directives', [
@ -133,10 +120,6 @@ export const opNotificationsModule = angular.module('openproject.notifications',
angular.module('openproject.inplace-edit', []);
angular.module('openproject.responsive', []);
export const filtersModule = angular.module('openproject.filters', [
'openproject.models'
]);
export const wpButtonsModule = angular.module('openproject.wpButtons',
['ui.router', 'ui.router.upgrade', 'openproject.services']);
@ -154,7 +137,6 @@ export const openprojectModule = angular.module('openproject', [
'openproject.timeEntries',
'ngAnimate',
'ngAria',
'ngSanitize',
angularDragula(angular),
'openproject.layout',
'openproject.api',
@ -163,7 +145,6 @@ export const openprojectModule = angular.module('openproject', [
'openproject.inplace-edit',
wpButtonsModule.name,
'openproject.responsive',
filtersModule.name
]);
export default openprojectModule;

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {APP_INITIALIZER, ApplicationRef, NgModule} from '@angular/core';
import {ApplicationRef, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {UpgradeModule} from '@angular/upgrade/static';
import {FormsModule} from '@angular/forms';
@ -70,13 +70,11 @@ import {
$stateToken,
$timeoutToken,
AutoCompleteHelperServiceToken,
FocusHelperToken,
HookServiceToken,
I18nToken,
TextileServiceToken,
upgradeService,
upgradeServiceWithToken,
wpDestroyModalToken,
upgradeServiceWithToken, WorkPackageServiceToken,
wpMoreMenuServiceToken
} from './angular4-transition-utils';
import {WpCustomActionComponent} from 'core-components/wp-custom-actions/wp-custom-actions/wp-custom-action.component';
@ -140,8 +138,6 @@ import {WorkPackageEditActionsBarComponent} from 'core-components/common/edit-ac
import {WorkPackageCopyFullViewComponent} from 'core-components/wp-copy/wp-copy-full-view.component';
import {WorkPackageNewSplitViewComponent} from 'core-components/wp-new/wp-new-split-view.component';
import {WorkPackageCopySplitViewComponent} from 'core-components/wp-copy/wp-copy-split-view.component';
import {FocusWithinDirective} from 'core-components/common/focus-within/focus-within.upgraded.directive';
import {ClickOnKeypressComponent} from 'core-app/ui_components/click-on-keypress-upgraded.component';
import {AutocompleteSelectDecorationComponent} from 'core-components/common/autocomplete-select-decoration/autocomplete-select-decoration.component';
import {OPContextMenuService} from 'core-components/op-context-menu/op-context-menu.service';
import {PortalModule} from '@angular/cdk/portal';
@ -195,7 +191,6 @@ import {FilterStringValueComponent} from 'core-components/filters/filter-string-
import {FilterBooleanValueComponent} from 'core-components/filters/filter-boolean-value/filter-boolean-value.component';
import {OpDatePickerComponent} from 'core-components/wp-edit/op-date-picker/op-date-picker.component';
import {AccessibleByKeyboardComponent} from 'core-components/a11y/accessible-by-keyboard.component';
import {WorkPackageFormQueryGroupComponent} from 'core-components/wp-form-group/wp-query-group.component';
import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-group/wp-attribute-group.component';
import {WorkPackageRelationsService} from 'core-components/wp-relations/wp-relations.service';
import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper';
@ -221,6 +216,11 @@ import {QuerySharingModal} from "core-components/modals/share-modal/query-sharin
import {SaveQueryModal} from "core-components/modals/save-modal/save-query.modal";
import {QuerySharingForm} from "core-components/modals/share-modal/query-sharing-form.component";
import {RenameQueryModal} from "core-components/modals/rename-query-modal/rename-query.modal";
import {FocusHelperService} from 'core-components/common/focus/focus-helper';
import {WpDestroyModal} from "core-components/modals/wp-destroy-modal/wp-destroy.modal";
import {FocusWithinDirective} from "core-components/common/focus/focus-within.upgraded.directive";
import {AccessibleClickDirective} from "core-components/a11y/accessible-click.directive";
import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp-relation-children/wp-children-query.component';
@NgModule({
imports: [
@ -244,13 +244,13 @@ import {RenameQueryModal} from "core-components/modals/rename-query-modal/rename
upgradeServiceWithToken('$timeout', $timeoutToken),
upgradeServiceWithToken('$locale', $localeToken),
upgradeServiceWithToken('textileService', TextileServiceToken),
upgradeServiceWithToken('WorkPackageService', WorkPackageServiceToken),
upgradeServiceWithToken('AutoCompleteHelper', AutoCompleteHelperServiceToken),
NotificationsService,
upgradeServiceWithToken('FocusHelper', FocusHelperToken),
FocusHelperService,
PathHelperService,
upgradeServiceWithToken('wpMoreMenuService', wpMoreMenuServiceToken),
TimezoneService,
upgradeServiceWithToken('wpDestroyModal', wpDestroyModalToken),
upgradeService('wpRelations', WorkPackageRelationsService),
UrlParamsHelperService,
WorkPackageCacheService,
@ -322,6 +322,7 @@ import {RenameQueryModal} from "core-components/modals/rename-query-modal/rename
OpIcon,
OpDatePickerComponent,
AccessibleByKeyboardComponent,
AccessibleClickDirective,
TablePaginationComponent,
WorkPackageTablePaginationComponent,
WorkPackageTimelineHeaderController,
@ -386,8 +387,7 @@ import {RenameQueryModal} from "core-components/modals/rename-query-modal/rename
WorkPackageAttachmentListItemComponent,
OpDateTimeComponent,
UserLinkComponent,
ClickOnKeypressComponent,
WorkPackageFormQueryGroupComponent,
WorkPackageChildrenQueryComponent,
WorkPackageFormAttributeGroupComponent,
// Activity Tab
@ -454,6 +454,7 @@ import {RenameQueryModal} from "core-components/modals/rename-query-modal/rename
SaveQueryModal,
QuerySharingForm,
RenameQueryModal,
WpDestroyModal,
// Notifications
NotificationsContainerComponent,
@ -523,6 +524,7 @@ import {RenameQueryModal} from "core-components/modals/rename-query-modal/rename
QuerySharingModal,
SaveQueryModal,
RenameQueryModal,
WpDestroyModal,
// Notifications
NotificationsContainerComponent,

@ -41,13 +41,12 @@ export const I18nToken = new InjectionToken<op.I18n>('I18n');
export const AutoCompleteHelperServiceToken = new InjectionToken<any>('AutoCompleteHelperServiceToken');
export const TextileServiceToken = new InjectionToken<any>('TextileServiceToken');
export const FocusHelperToken = new InjectionToken<any>('FocusHelper');
export const wpMoreMenuServiceToken = new InjectionToken<any>('wpMoreMenuService');
export const $httpToken = new InjectionToken<any>('$http');
export const wpDestroyModalToken = new InjectionToken<any>('wpDestroyModal');
export const OpContextMenuLocalsToken = new InjectionToken<any>('CONTEXT_MENU_LOCALS');
export const OpModalLocalsToken = new InjectionToken<any>('OP_MODAL_LOCALS');
export const HookServiceToken = new InjectionToken<any>('HookService');
export const WorkPackageServiceToken = new InjectionToken<any>('WorkPackageService');
export function upgradeService(ng1InjectorName:string, providedType:any) {
return {

@ -31,8 +31,7 @@ import {Component, EventEmitter, Input, Output} from '@angular/core';
@Component({
selector: 'accessible-by-keyboard',
template: `
<a (click)="handleClick($event)"
(keydown.enter)="handleClick($event)"
<a (accessibleClick)="handleClick($event)"
role="link"
[ngClass]="linkClass"
[attr.disabled]="isDisabled || undefined"

@ -27,6 +27,7 @@
//++
import {opUiComponentsModule} from 'core-app/angular-modules';
import {keyCodes} from "core-components/common/keyCodes.enum";
opUiComponentsModule.directive(
'accessibleByKeyboard',
@ -34,6 +35,13 @@ opUiComponentsModule.directive(
return {
restrict: 'E',
transclude: true,
link: function(scope:any) {
scope.executeOnEnter = (event:JQueryEventObject) => {
if (!scope.isDisabled && (event.which === keyCodes.ENTER || event.which === keyCodes.SPACE)) {
scope.execute(event);
}
};
},
scope: {
execute: '&',
isDisabled: '=',
@ -49,7 +57,6 @@ opUiComponentsModule.directive(
ng-disabled="isDisabled"
title='{{ linkTitle }}'
aria-label="{{ linkAriaLabel }}"
data-click-on-keypress="[13, 32]"
href>
<span ng-transclude class='{{ spanClass }}'></span>
</a>

@ -0,0 +1,86 @@
// -- 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.
// ++
import {Component, DebugElement} from "@angular/core";
require('core-app/angular4-test-setup');
import {async, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {AccessibleByKeyboardComponent} from 'core-components/a11y/accessible-by-keyboard.component';
import {ComponentFixture} from '@angular/core/testing/src/component_fixture';
import {AccessibleClickDirective} from "core-components/a11y/accessible-click.directive";
import {By} from "@angular/platform-browser";
@Component({
template: `<div (accessibleClick)="onClick()">Click me</div>`
})
class TestAccessibleClickDirective {
public onClick() {
}
}
describe('accessibleByKeyboard component', () => {
beforeEach(async(() => {
// noinspection JSIgnoredPromiseFromCall
TestBed.configureTestingModule({
declarations: [
AccessibleClickDirective,
TestAccessibleClickDirective
]
}).compileComponents();
}));
describe('triggering the click handler', () => {
let app:TestAccessibleClickDirective;
let fixture:ComponentFixture<TestAccessibleClickDirective>;
let element:DebugElement;
it('should render an inner link with specified classes', fakeAsync(() => {
fixture = TestBed.createComponent(TestAccessibleClickDirective);
app = fixture.debugElement.componentInstance;
element = fixture.debugElement.query(By.css('div'));
const spy = sinon.spy(app, 'onClick');
fixture.detectChanges();
// Trigger click
element.triggerEventHandler('click', {type: 'click'});
element.triggerEventHandler('keyup', {type: 'keyup', which: 13});
element.triggerEventHandler('keyup', {type: 'keyup', which: 32});
tick();
fixture.detectChanges();
expect(spy).to.have.been.calledThrice;
}));
});
});

@ -1,6 +1,6 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
// Copyright (C) 2012-2017 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.
@ -23,23 +23,26 @@
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function($http, PathHelper) {
import {Directive, EventEmitter, HostListener, Output} from '@angular/core';
import {keyCodes} from "core-components/common/keyCodes.enum";
var StatusService = {
getStatuses: function() {
return StatusService.doQuery(PathHelper.apiV3StatusesPath());
},
@Directive({
selector: '[accessibleClick]',
})
export class AccessibleClickDirective {
@Output('accessibleClick') onClick = new EventEmitter<JQueryEventObject>();
doQuery: function(url, params) {
return $http.get(url, { params: params })
.then(function(response){
return response.data._embedded.elements;
});
@HostListener('click', ['$event'])
@HostListener('keyup', ['$event'])
public handleClick(event:JQueryEventObject) {
if (event.type === 'click' || event.which === keyCodes.ENTER || event.which === keyCodes.SPACE) {
this.onClick.emit(event);
}
};
return StatusService;
};
event.preventDefault();
event.stopPropagation();
}
}

@ -1,10 +0,0 @@
<a data-ng-click='isDisabled || execute({ "$event": $event })'
role="link"
class='{{ linkClass }}'
ng-disabled="isDisabled"
title='{{ linkTitle }}'
aria-label="{{ linkAriaLabel }}"
data-click-on-keypress="[13, 32]"
href>
<span ng-transclude class='{{ spanClass }}'></span>
</a>

@ -0,0 +1,16 @@
// Based on wrapper by @banjankri
// https://github.com/angular/angular/issues/16695#issuecomment-336456199
import {downgradeComponent} from "@angular/upgrade/static";
export function downgradeAttributeDirective(componentClass:new(...args:any[]) => any) {
const wrapper = function($compile:any, $injector:any, $parse:any) {
const factory = downgradeComponent({ component: componentClass });
const component = factory($compile, $injector, $parse);
component.restrict = "AE";
return component;
};
wrapper.$inject = ["$compile", "$injector", "$parse"];
return wrapper;
}

@ -1,36 +0,0 @@
// -- 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.
// ++
import {filtersModule} from './../../../angular-modules';
function htmlEscape() {
return function(string:string) {
return _.escape(string);
};
}
filtersModule.filter('htmlEscape', htmlEscape);

@ -0,0 +1,110 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details.
//++
import {opServicesModule} from 'core-app/angular-modules';
import {downgradeInjectable} from '@angular/upgrade/static';
import {Injectable} from '@angular/core';
@Injectable()
export class FocusHelperService {
private minimumOffsetForNewSwitchInMs = 100;
private lastFocusSwitch = -this.minimumOffsetForNewSwitchInMs;
private lastPriority = -1;
private static FOCUSABLE_SELECTORS = ['a, button, :input, [tabindex], select'];
public throttleAndCheckIfAllowedFocusChangeBasedOnTimeout() {
var allowFocusSwitch = (Date.now() - this.lastFocusSwitch) >= this.minimumOffsetForNewSwitchInMs;
// Always update so that a chain of focus-change-requests gets considered as one
this.lastFocusSwitch = Date.now();
return allowFocusSwitch;
}
public checkIfAllowedFocusChange(priority?:any) {
var checkTimeout = this.throttleAndCheckIfAllowedFocusChangeBasedOnTimeout();
if (checkTimeout) {
// new timeout window -> reset priority
this.lastPriority = -1;
} else {
// within timeout window
if (priority > this.lastPriority) {
this.lastPriority = priority;
return true;
}
}
return checkTimeout;
}
public getFocusableElement(element:JQuery) {
var focusser = element.find('input.ui-select-focusser');
if (focusser.length > 0) {
return focusser[0];
}
var focusable = element;
if (!element.is(FocusHelperService.FOCUSABLE_SELECTORS)) {
focusable = element.find(FocusHelperService.FOCUSABLE_SELECTORS);
}
return focusable[0];
}
public focus(element:JQuery) {
var focusable = angular.element(this.getFocusableElement(element)),
$focusable = angular.element(focusable),
isDisabled = $focusable.is('[disabled]');
if (isDisabled && !$focusable.attr('ng-disabled')) {
$focusable.prop('disabled', false);
}
focusable.focus();
if (isDisabled && !$focusable.attr('ng-disabled')) {
$focusable.prop('disabled', true);
}
}
public focusElement(element:JQuery, priority?:any) {
if (!this.checkIfAllowedFocusChange(priority)) {
return;
}
setTimeout(() => {
this.focus(element);
});
}
}
opServicesModule.service('FocusHelper', downgradeInjectable(FocusHelperService));

@ -26,35 +26,31 @@
// See docs/COPYRIGHT.rdoc for more details.
//++
import {Directive, ElementRef, Input, OnInit} from '@angular/core';
import {AfterViewInit, Directive, ElementRef, Input} from "@angular/core";
import {FocusHelperService} from "core-components/common/focus/focus-helper";
import {opUiComponentsModule} from "core-app/angular-modules";
import {downgradeAttributeDirective} from "core-components/angular/downgrade-attribute-directive";
@Directive({
selector: '[click-on-keypress-upgraded]'
selector: '[focus]'
})
export class ClickOnKeypressComponent implements OnInit {
@Input('clickOnKeypressKeys') keys:number[];
export class FocusDirective implements AfterViewInit {
@Input('focus') condition:boolean;
@Input('focusPriority') priority?:number = 0;
constructor(readonly elementRef:ElementRef) {
constructor(readonly FocusHelper:FocusHelperService,
readonly elementRef:ElementRef) {
}
ngOnInit() {
const element = angular.element(this.elementRef.nativeElement);
element.on('keydown', this.doIfWatchedKey.bind(this));
element.on('keyup', (evt:JQueryKeyEventObject) => {
this.doIfWatchedKey(evt, () => angular.element(evt.target).click());
});
ngAfterViewInit() {
this.updateFocus();
}
private doIfWatchedKey(evt:JQueryKeyEventObject, callback?:(evt:JQueryKeyEventObject) => void) {
if (this.keys.indexOf(evt.which) !== -1) {
evt.stopPropagation();
evt.preventDefault();
if (callback !== undefined) {
callback(evt);
}
private updateFocus() {
if (this.condition) {
this.FocusHelper.focusElement(this.elementRef.nativeElement, this.priority);
}
}
}
opUiComponentsModule. directive('focus', downgradeAttributeDirective(FocusDirective));

@ -34,7 +34,7 @@ describe('authorisationService', function() {
var authorisationService:AuthorisationService, $rootScope:ng.IRootScopeService, query:any;
beforeEach(angular.mock.module('openproject.services', 'openproject.models'));
beforeEach(angular.mock.module('openproject.services'));
beforeEach(inject(function(_authorisationService_:AuthorisationService, _$rootScope_:ng.IRootScopeService){
authorisationService = _authorisationService_;

@ -32,7 +32,7 @@ const expect = chai.expect;
describe('wpFiltersService', () => {
var wpFiltersService:WorkPackageFiltersService;
beforeEach(angular.mock.module('openproject.filters'));
beforeEach(angular.mock.module('openproject.services'));
beforeEach(angular.mock.inject((_wpFiltersService_:any) => {
wpFiltersService = _wpFiltersService_;
}));

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {filtersModule} from '../../../angular-modules';
import {opServicesModule} from '../../../angular-modules';
export default class WorkPackageFiltersService {
public visible:boolean = false;
@ -36,4 +36,4 @@ export default class WorkPackageFiltersService {
}
}
filtersModule.service('wpFiltersService', WorkPackageFiltersService);
opServicesModule.service('wpFiltersService', WorkPackageFiltersService);

@ -67,7 +67,12 @@ export class WpTableExportModal extends OpModalComponent implements OnInit {
return column.id;
});
return href + "&" + this.UrlParamsHelper.buildQueryString({ 'columns[]': columnIds });
let url = URI(href);
// Remove current columns
url.removeSearch('columns[]');
url.addSearch('columns[]', columnIds);
return url.toString();
}
protected get afterFocusOn():JQuery {

@ -90,6 +90,6 @@ export class RenameQueryModal extends OpModalComponent implements OnInit {
.then(() => {
this.closeMe($event);
})
.catch((error) => this.wpNotificationsService.handleRawError(error));
.catch((error) => this.wpNotificationsService.handleErrorResponse(error));
};
}

@ -14,9 +14,7 @@ export interface QuerySharingChange {
selector: 'query-sharing-form',
template: require('!!raw-loader!./query-sharing-form.html')
})
export class QuerySharingForm implements OnInit {
public canPublish:boolean = false;
export class QuerySharingForm {
@Input() public isSave:boolean;
@Input() public isStarred:boolean;
@Input() public isPublic:boolean;
@ -32,17 +30,19 @@ export class QuerySharingForm implements OnInit {
@Inject(I18nToken) readonly I18n:op.I18n) {
}
ngOnInit() {
const form = this.states.query.form.value!;
this.canPublish = form.schema.public.writable;
}
public get canStar() {
return this.isSave ||
this.authorisationService.can('query', 'star') ||
this.authorisationService.can('query', 'unstar');
}
public get canPublish() {
const form = this.states.query.form.value!;
return this.authorisationService.can('query', 'saveImmediately')
&& form.schema.public.writable;
}
public updateStarred(val:boolean) {
this.isStarred = val;
this.changed();

@ -1,105 +0,0 @@
//-- 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.
//++
import {wpControllersModule} from '../../../angular-modules';
import {States} from '../../states.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';
import {StateService} from '@uirouter/core';
export class WorkPackageDestroyModalController {
public text:any;
public workPackages:WorkPackageResource[];
public workPackageLabel:string;
constructor(private $scope:any,
private $state:StateService,
private states:States,
private WorkPackageService:any,
private wpTableFocus:WorkPackageTableFocusService,
private I18n:op.I18n,
private wpDestroyModal:any) {
this.workPackages = $scope.workPackages;
this.workPackageLabel = I18n.t('js.units.workPackage', { count: this.workPackages.length });
this.text = {
close: I18n.t('js.close_popup_title'),
cancel: I18n.t('js.button_cancel'),
confirm: I18n.t('js.button_confirm'),
warning: I18n.t('js.label_warning'),
title: I18n.t('js.modals.destroy_work_package.title', { label: this.workPackageLabel }),
text: I18n.t('js.modals.destroy_work_package.text', { label: this.workPackageLabel, count: this.workPackages.length }),
childCount: (wp:WorkPackageResource) => {
const count = this.children(wp).length;
return this.I18n.t('js.units.child_work_packages', {count: count});
},
hasChildren: (wp:WorkPackageResource) =>
I18n.t('js.modals.destroy_work_package.has_children', {childUnits: this.text.childCount(wp) }),
deletesChildren: I18n.t('js.modals.destroy_work_package.deletes_children')
};
}
public $onInit() {
// Created for interface compliance
}
public close() {
try {
this.wpDestroyModal.deactivate();
} catch(e) {
console.error("Failed to close deletion modal: " + e);
}
}
public confirmDeletion() {
this.wpDestroyModal.deactivate();
this.WorkPackageService.performBulkDelete(this.workPackages.map(el => el.id), true)
.then(() => {
this.close();
this.wpTableFocus.clear();
this.$state.go('work-packages.list');
});
}
public childLabel (workPackage:WorkPackageResource) {
}
public children(workPackage:WorkPackageResource) {
if (workPackage.hasOwnProperty('children')) {
return workPackage.children;
} else {
return [];
}
}
}
wpControllersModule.controller('WorkPackageDestroyModalController', WorkPackageDestroyModalController);

@ -1,76 +0,0 @@
<div class="op-modal--portal" id="wp_destroy_modal">
<div class="op-modal--modal-container">
<div class="op-modal--modal-header">
<a>
<i class="icon-close"
ng-click="$ctrl.close()"
title="{{ ::$ctrl.text.close }}">
</i>
</a>
</div>
<form name="modalWpDestroyForm" class="form danger-zone">
<section class="form--section -inner-scrolling">
<h3 class="form--section-title" ng-bind="::$ctrl.text.title"></h3>
<div ng-if="$ctrl.workPackages.length === 1"
class="modal-inner-scrolling-container"
ng-init="workPackage = $ctrl.workPackages[0]; children = $ctrl.children(workPackage)">
<p>
<span ng-bind="$ctrl.text.text"></span>
<br/>
<strong>
{{ workPackage.type.name }}
#{{ workPackage.id }}
{{ workPackage.subject }}
</strong>
</p>
<div ng-if="children.length > 0">
<p class="danger-zone--warning">
<span class="icon icon-error"></span>
<strong ng-bind="$ctrl.text.warning"></strong>:
<span ng-bind="$ctrl.text.hasChildren(workPackage)"></span>
</p>
<ul>
<li ng-repeat="child in children track by $index">
#<span ng-bind="child.id"></span>
<span ng-bind="child.subject"></span>
</li>
</ul>
<p>
<span ng-bind="$ctrl.text.deletesChildren"></span>
</p>
</div>
</div>
<div class="modal-inner-scrolling-container" ng-if="$ctrl.workPackages.length > 1">
<p class="danger-zone--warning">
<span class="icon icon-error"></span>
<strong ng-bind="$ctrl.text.text"></strong>
</p>
<ul>
<li ng-repeat="wp in $ctrl.workPackages track by $index">
#<span ng-bind="wp.id"></span>
<span ng-bind="wp.subject"></span>
<strong ng-if="$ctrl.children(wp).length > 0">
(&plus; {{ $ctrl.text.childCount(wp) }})
</strong>
</li>
</ul>
</div>
</section>
<div class="form--space -left-spacing">
<button class="button -danger"
ng-bind="::$ctrl.text.confirm"
ng-click="$ctrl.confirmDeletion()">
</button>
<button class="button"
ng-bind="::$ctrl.text.cancel"
ng-click="$ctrl.close()">
</button>
</div>
</form>
</div>
</div>

@ -1,39 +0,0 @@
//-- 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.
//++
import {wpControllersModule} from '../../../angular-modules';
function wpDestroyService(btfModal:any) {
return btfModal({
controller: 'WorkPackageDestroyModalController',
controllerAs: '$ctrl',
templateUrl: '/components/modals/wp-destroy-modal/wp-destroy-modal.html'
});
}
wpControllersModule.factory('wpDestroyModal', wpDestroyService);

@ -0,0 +1,78 @@
<div class="op-modal--portal ">
<div id="wp_destroy_modal"
class="op-modal--modal-container wp-table--configuration-modal loading-indicator--location"
data-indicator-name="modal"
tabindex="0">
<div class="op-modal--modal-header">
<a class="op-modal--modal-close-button">
<i
class="icon-close"
(click)="closeMe($event)"
[attr.title]="text.close">
</i>
</a>
</div>
<form name="modalWpDestroyForm" class="form danger-zone">
<section class="form--section -inner-scrolling">
<h3 class="form--section-title" [textContent]="text.title"></h3>
<div *ngIf="singleWorkPackage"
class="modal-inner-scrolling-container">
<p>
<span [textContent]="text.text"></span>
<br/>
<strong>
{{ singleWorkPackage.type.name }}
#{{ singleWorkPackage.id }}
{{ singleWorkPackage.subject }}
</strong>
</p>
<div *ngIf="singleWorkPackageChildren && singleWorkPackageChildren.length > 0">
<p class="danger-zone--warning">
<span class="icon icon-error"></span>
<strong [textContent]="text.warning"></strong>:
<span [textContent]="text.hasChildren(singleWorkPackage)"></span>
</p>
<ul>
<li *ngFor="let child of singleWorkPackageChildren">
#<span [textContent]="child.id"></span>
<span [textContent]="child.subject"></span>
</li>
</ul>
<p>
<span [textContent]="text.deletesChildren"></span>
</p>
</div>
</div>
<div class="modal-inner-scrolling-container" *ngIf="workPackages.length > 1">
<p class="danger-zone--warning">
<span class="icon icon-error"></span>
<strong [textContent]="text.text"></strong>
</p>
<ul>
<li *ngFor="let wp of workPackages">
#<span [textContent]="wp.id"></span>
<span [textContent]="wp.subject"></span>
<strong *ngIf="children(wp).length > 0">
(+ {{ text.childCount(wp) }})
</strong>
</li>
</ul>
</div>
</section>
<div class="form--space -left-spacing">
<button class="button -danger"
[textContent]="text.confirm"
(click)="confirmDeletion($event)">
</button>
<button class="button"
[textContent]="text.cancel"
(click)="closeMe($event)">
</button>
</div>
</form>
</div>
</div>

@ -0,0 +1,118 @@
//-- 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.
//++
import {WorkPackagesListService} from '../../wp-list/wp-list.service';
import {States} from '../../states.service';
import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service';
import {NotificationsService} from "core-components/common/notifications/notifications.service";
import {OpModalComponent} from "core-components/op-modals/op-modal.component";
import {Component, ElementRef, Inject, OnInit} from "@angular/core";
import {$stateToken, I18nToken, OpModalLocalsToken, WorkPackageServiceToken} from "core-app/angular4-transition-utils";
import {OpModalLocalsMap} from "core-components/op-modals/op-modal.types";
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';
import {StateService} from '@uirouter/core';
@Component({
template: require('!!raw-loader!./wp-destroy.modal.html')
})
export class WpDestroyModal extends OpModalComponent implements OnInit {
// When deleting multiple
public workPackages:WorkPackageResource[];
public workPackageLabel:string;
// Single work package
public singleWorkPackage:WorkPackageResource;
public singleWorkPackageChildren:WorkPackageResource[];
public text:{ [key:string]:any } = {
label_visibility_settings: this.I18n.t('js.label_visibility_settings'),
button_save: this.I18n.t('js.modals.button_save'),
confirm: this.I18n.t('js.button_confirm'),
warning: this.I18n.t('js.label_warning'),
cancel: this.I18n.t('js.button_cancel'),
close: this.I18n.t('js.close_popup_title'),
};
constructor(readonly elementRef:ElementRef,
@Inject(WorkPackageServiceToken) readonly WorkPackageService:any,
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
@Inject(I18nToken) readonly I18n:op.I18n,
@Inject($stateToken) readonly $state:StateService,
readonly states:States,
readonly wpTableFocus:WorkPackageTableFocusService,
readonly wpListService:WorkPackagesListService,
readonly wpNotificationsService:WorkPackageNotificationService,
readonly notificationsService:NotificationsService) {
super(locals, elementRef);
}
ngOnInit() {
super.ngOnInit();
this.workPackages = this.locals.workPackages;
this.workPackageLabel = this.I18n.t('js.units.workPackage', { count: this.workPackages.length });
// Ugly way to provide the same view bindings as the ng-init in the previous template.
if (this.workPackages.length === 1) {
this.singleWorkPackage = this.workPackages[0];
this.singleWorkPackageChildren = this.singleWorkPackage.children;
}
this.text.title = this.I18n.t('js.modals.destroy_work_package.title', { label: this.workPackageLabel }),
this.text.text = this.I18n.t('js.modals.destroy_work_package.text', { label: this.workPackageLabel, count: this.workPackages.length }),
this.text.childCount = (wp:WorkPackageResource) => {
const count = this.children(wp).length;
return this.I18n.t('js.units.child_work_packages', {count: count});
};
this.text.hasChildren = (wp:WorkPackageResource) =>
this.I18n.t('js.modals.destroy_work_package.has_children', {childUnits: this.text.childCount(wp) }),
this.text.deletesChildren = this.I18n.t('js.modals.destroy_work_package.deletes_children');
}
public confirmDeletion($event:JQueryEventObject) {
this.WorkPackageService.performBulkDelete(this.workPackages.map(el => el.id), true)
.then(() => {
this.closeMe($event);
this.wpTableFocus.clear();
this.$state.go('work-packages.list');
});
}
public children(workPackage:WorkPackageResource) {
if (workPackage.hasOwnProperty('children')) {
return workPackage.children;
} else {
return [];
}
}
}

@ -161,11 +161,11 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements OnD
},
{
// Query save as modal
disabled: this.authorisationService.cannot('query', 'updateImmediately'),
disabled: this.form ? !this.form.$links.create_new : this.authorisationService.cannot('query', 'updateImmediately'),
linkText: this.I18n.t('js.toolbar.settings.save_as'),
icon: 'icon-save',
onClick: ($event:JQueryEventObject) => {
if (this.allowFormAction($event, 'commit')) {
if (this.allowFormAction($event, 'create_new')) {
this.opModalService.show(SaveQueryModal);
}
@ -214,7 +214,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements OnD
},
{
// Settings modal
disabled: !this.query.id || this.authorisationService.cannot('query', 'update'),
disabled: !this.query.id || this.authorisationService.cannot('query', 'updateImmediately'),
linkText: this.I18n.t('js.toolbar.settings.page_settings'),
icon: 'icon-settings',
onClick: ($event:JQueryEventObject) => {
@ -231,7 +231,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements OnD
},
{
// Settings modal
disabled: !this.query.results.customFields,
hidden: !this.query.results.customFields,
href: this.query.results.customFields && this.query.results.customFields.href,
linkText: this.query.results.customFields && this.query.results.customFields.name,
icon: 'icon-custom-fields',

@ -13,8 +13,7 @@
[attr.href]="item.href || ''"
[attr.aria-disabled]="item.disabled"
[attr.aria-label]="item.ariaLabel || item.linkText"
(keydown.enter)="handleClick(item, $event)"
(click)="handleClick(item, $event)">
(accessibleClick)="handleClick(item, $event)">
<op-icon *ngIf="item.icon" icon-classes="icon-action-menu {{ item.icon }}"> </op-icon>
<span [textContent]="item.linkText"></span>
</a>

@ -5,10 +5,11 @@ import {
import {ComponentPortal, DomPortalOutlet, PortalInjector} from "@angular/cdk/portal";
import {TransitionService} from "@uirouter/core";
import {OpContextMenuHandler} from "core-components/op-context-menu/op-context-menu-handler";
import {FocusHelperToken, OpContextMenuLocalsToken} from "core-app/angular4-transition-utils";
import {OpContextMenuLocalsToken} from "core-app/angular4-transition-utils";
import {OpContextMenuLocalsMap} from "core-components/op-context-menu/op-context-menu.types";
import {OPContextMenuComponent} from "core-components/op-context-menu/op-context-menu.component";
import {keyCodes} from 'core-components/common/keyCodes.enum';
import {FocusHelperService} from 'core-components/common/focus/focus-helper';
@Injectable()
export class OPContextMenuService {
@ -23,7 +24,7 @@ export class OPContextMenuService {
private isOpening = false;
constructor(private componentFactoryResolver:ComponentFactoryResolver,
@Inject(FocusHelperToken) readonly FocusHelper:any,
readonly FocusHelper:FocusHelperService,
private appRef:ApplicationRef,
private $transitions:TransitionService,
private injector:Injector) {

@ -2,8 +2,7 @@ import {Directive, ElementRef, Inject, Input} from "@angular/core";
import {WorkPackageAction} from "core-components/wp-table/context-menu-helper/wp-context-menu-helper.service";
import {
$stateToken,
HookServiceToken,
wpDestroyModalToken
HookServiceToken
} from "core-app/angular4-transition-utils";
import {LinkHandling} from "core-components/common/link-handling/link-handling";
import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service";
@ -13,6 +12,8 @@ import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-
import {WorkPackageAuthorization} from "core-components/work-packages/work-package-authorization.service";
import {AuthorisationService} from "core-components/common/model-auth/model-auth.service";
import {StateService} from "@uirouter/core";
import {OpModalService} from "core-components/op-modals/op-modal.service";
import {WpDestroyModal} from "core-components/modals/wp-destroy-modal/wp-destroy.modal";
@Directive({
selector: '[wpSingleContextMenu]'
@ -21,9 +22,9 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
@Input('wpSingleContextMenu-workPackage') public workPackage:WorkPackageResource;
constructor(@Inject(HookServiceToken) readonly HookService:any,
@Inject(wpDestroyModalToken) readonly wpDestroyModal:any,
@Inject($stateToken) readonly $state:StateService,
readonly elementRef:ElementRef,
readonly opModalService:OpModalService,
readonly opContextMenuService:OPContextMenuService,
readonly authorisationService:AuthorisationService) {
super(elementRef, opContextMenuService);
@ -50,7 +51,7 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
this.$state.go('work-packages.copy', {copiedFromWorkPackageId: this.workPackage.id});
break;
case 'delete':
this.wpDestroyModal.activate({workPackages: [this.workPackage]});
this.opModalService.show(WpDestroyModal, {workPackages: [this.workPackage]});
break;
default:
@ -78,7 +79,7 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
private getPermittedPluginActions(authorization:WorkPackageAuthorization) {
var pluginActions:WorkPackageAction[] = [];
angular.forEach(this.HookService.call('workPackageDetailsMoreMenu'), function (action) {
angular.forEach(this.HookService.call('workPackageDetailsMoreMenu'), function(action) {
pluginActions = pluginActions.concat(action);
});

@ -6,7 +6,7 @@ import {
import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table";
import {States} from "core-components/states.service";
import {WorkPackageRelationsHierarchyService} from "core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service";
import {$stateToken, wpDestroyModalToken} from "core-app/angular4-transition-utils";
import {$stateToken} from "core-app/angular4-transition-utils";
import {WorkPackageTableSelection} from "core-components/wp-fast-table/state/wp-table-selection.service";
import {LinkHandling} from "core-components/common/link-handling/link-handling";
import {OpContextMenuHandler} from "core-components/op-context-menu/op-context-menu-handler";
@ -16,12 +16,14 @@ import {
OpContextMenuLocalsMap
} from "core-components/op-context-menu/op-context-menu.types";
import {PERMITTED_CONTEXT_MENU_ACTIONS} from "core-components/op-context-menu/wp-context-menu/wp-static-context-menu-actions";
import {OpModalService} from "core-components/op-modals/op-modal.service";
import {WpDestroyModal} from "core-components/modals/wp-destroy-modal/wp-destroy.modal";
export class OpWorkPackageContextMenu extends OpContextMenuHandler {
private states = this.injector.get(States);
private wpRelationsHierarchyService = this.injector.get(WorkPackageRelationsHierarchyService);
private wpDestroyModal = this.injector.get(wpDestroyModalToken);
private opModalService:OpModalService = this.injector.get(OpModalService);
private $state = this.injector.get($stateToken);
private wpTableSelection = this.injector.get(WorkPackageTableSelection);
private WorkPackageContextMenuHelper = this.injector.get(WorkPackageContextMenuHelperService);
@ -89,7 +91,7 @@ export class OpWorkPackageContextMenu extends OpContextMenuHandler {
private deleteSelectedWorkPackages() {
var selected = this.getSelectedWorkPackages();
this.wpDestroyModal.activate({workPackages: selected});
this.opModalService.show(WpDestroyModal, {workPackages: selected});
}
private editSelectedWorkPackages(link:any) {

@ -7,11 +7,12 @@ import {
} from '@angular/core';
import {ComponentPortal, ComponentType, DomPortalOutlet, PortalInjector} from '@angular/cdk/portal';
import {TransitionService} from '@uirouter/core';
import {FocusHelperToken, OpModalLocalsToken} from 'core-app/angular4-transition-utils';
import {OpModalLocalsToken} from 'core-app/angular4-transition-utils';
import {OpModalComponent} from 'core-components/op-modals/op-modal.component';
import {keyCodes} from 'core-components/common/keyCodes.enum';
import {opServicesModule} from "core-app/angular-modules";
import {downgradeInjectable} from "@angular/upgrade/static";
import {FocusHelperService} from 'core-components/common/focus/focus-helper';
@Injectable()
export class OpModalService {
@ -26,7 +27,7 @@ export class OpModalService {
private opening:boolean = false;
constructor(private componentFactoryResolver:ComponentFactoryResolver,
@Inject(FocusHelperToken) readonly FocusHelper:any,
readonly FocusHelper:FocusHelperService,
private appRef:ApplicationRef,
private $transitions:TransitionService,
private injector:Injector) {

@ -34,8 +34,7 @@ describe('currentProject service', function() {
var element:ng.IAugmentedJQuery;
var currentProject:CurrentProjectService;
beforeEach(angular.mock.module('openproject.filters',
'openproject.templates',
beforeEach(angular.mock.module('openproject.templates',
'openproject.services'));
beforeEach(angular.mock.inject((_currentProject_:CurrentProjectService) => {

@ -50,6 +50,14 @@ const panels = {
};
},
get relations() {
return {
url: '/relations',
reloadOnSearch: false,
component: WorkPackageRelationsTabComponent,
};
},
get watchers() {
return {
url: '/watchers',
@ -133,10 +141,7 @@ openprojectModule
})
.state('work-packages.show.activity', panels.activity)
.state('work-packages.show.activity.details', panels.activityDetails)
.state('work-packages.show.relations', {
url: '/relations',
redirectTo: 'work-packages.show.activity'
})
.state('work-packages.show.relations', panels.relations)
.state('work-packages.show.watchers', panels.watchers)
.state('work-packages.list', {
@ -183,10 +188,7 @@ openprojectModule
.state('work-packages.list.details.overview', panels.overview)
.state('work-packages.list.details.activity', panels.activity)
.state('work-packages.list.details.activity.details', panels.activityDetails)
.state('work-packages.list.details.relations', {
url: '/relations',
redirectTo: 'work-packages.list.details.overview'
})
.state('work-packages.list.details.relations', panels.relations)
.state('work-packages.list.details.watchers', panels.watchers);
})

@ -69,6 +69,12 @@
uiSrefActive="selected">
<a href="" [textContent]="text.tabs.activity"></a>
</li>
<li uiSref="work-packages.show.relations"
[uiParams]="{workPackageId: workPackage.id}"
uiSrefActive="selected">
<a href="" [textContent]="text.tabs.relations"></a>
<wp-relations-count [wpId]="workPackage.id"></wp-relations-count>
</li>
<li *ngIf="canViewWorkPackageWatchers()"
uiSref="work-packages.show.watchers"
[uiParams]="{workPackageId: workPackage.id}"

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save