Merged with dev

pull/8481/head
Aleix Suau 4 years ago
commit 0323eb3015
  1. 53
      app/contracts/attribute_help_texts/base_contract.rb
  2. 34
      app/contracts/attribute_help_texts/create_contract.rb
  3. 34
      app/contracts/attribute_help_texts/update_contract.rb
  4. 33
      app/controllers/attribute_help_texts_controller.rb
  5. 5
      app/models/attribute_help_text.rb
  6. 54
      app/models/attribute_help_text/project.rb
  7. 9
      app/models/attribute_help_text/work_package.rb
  8. 15
      app/services/attribute_help_texts/create_service.rb
  9. 35
      app/services/attribute_help_texts/set_attributes_service.rb
  10. 35
      app/services/attribute_help_texts/update_service.rb
  11. 18
      app/views/attribute_help_texts/_form.html.erb
  12. 78
      app/views/attribute_help_texts/_tab.html.erb
  13. 105
      app/views/attribute_help_texts/index.html.erb
  14. 2
      config/initializers/menus.rb
  15. 2
      config/locales/crowdin/lt.yml
  16. 2
      config/locales/crowdin/tr.yml
  17. 1
      config/locales/en.yml
  18. 22
      frontend/src/app/components/wp-card-view/event-handler/click-handler.ts
  19. 10
      frontend/src/app/components/wp-card-view/event-handler/double-click-handler.ts
  20. 10
      frontend/src/app/components/wp-card-view/wp-card-view.component.ts
  21. 7
      frontend/src/app/components/wp-fast-table/builders/ui-state-link-builder.ts
  22. 8
      frontend/src/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts
  23. 9
      frontend/src/app/components/wp-fast-table/handlers/cell/relations-cell-handler.ts
  24. 5
      frontend/src/app/components/wp-fast-table/handlers/click-or-enter-handler.ts
  25. 10
      frontend/src/app/components/wp-fast-table/handlers/context-menu/context-menu-click-handler.ts
  26. 15
      frontend/src/app/components/wp-fast-table/handlers/context-menu/context-menu-handler.ts
  27. 12
      frontend/src/app/components/wp-fast-table/handlers/context-menu/context-menu-keyboard-handler.ts
  28. 19
      frontend/src/app/components/wp-fast-table/handlers/context-menu/context-menu-rightclick-handler.ts
  29. 18
      frontend/src/app/components/wp-fast-table/handlers/row/click-handler.ts
  30. 16
      frontend/src/app/components/wp-fast-table/handlers/row/double-click-handler.ts
  31. 20
      frontend/src/app/components/wp-fast-table/handlers/row/group-row-handler.ts
  32. 8
      frontend/src/app/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts
  33. 18
      frontend/src/app/components/wp-fast-table/handlers/row/wp-state-links-handler.ts
  34. 40
      frontend/src/app/components/wp-fast-table/handlers/table-handler-registry.ts
  35. 12
      frontend/src/app/components/wp-grid/wp-grid.component.ts
  36. 18
      frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts
  37. 4
      frontend/src/app/components/wp-table/embedded/wp-embedded-table.html
  38. 8
      frontend/src/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts
  39. 21
      frontend/src/app/components/wp-table/wp-table.component.ts
  40. 47
      frontend/src/app/modules/bim/ifc_models/bcf/list-container/bcf-list-container.component.ts
  41. 4
      frontend/src/app/modules/bim/ifc_models/bcf/list-container/bfc-list-container.component.html
  42. 22
      frontend/src/app/modules/bim/ifc_models/ifc-base-view/event-handler/bcf-card-view-handler-registry.ts
  43. 22
      frontend/src/app/modules/bim/ifc_models/ifc-base-view/event-handler/bcf-click-handler.ts
  44. 17
      frontend/src/app/modules/bim/ifc_models/ifc-base-view/event-handler/bcf-double-click-handler.ts
  45. 3
      frontend/src/app/modules/boards/board/board-list/board-list.component.html
  46. 9
      frontend/src/app/modules/boards/board/board-list/board-list.component.ts
  47. 2
      frontend/src/app/modules/common/back-routing/back-routing.service.ts
  48. 18
      frontend/src/app/modules/common/help-texts/attribute-help-text.modal.ts
  49. 4
      frontend/src/app/modules/common/help-texts/help-text.modal.html
  50. 12
      frontend/src/app/modules/common/openproject-common.module.ts
  51. 2
      frontend/src/app/modules/common/tabs/content-tabs/content-tabs.component.ts
  52. 1
      frontend/src/app/modules/grids/widgets/custom-text/custom-text.component.html
  53. 1
      frontend/src/app/modules/grids/widgets/documents/documents.component.html
  54. 4
      frontend/src/app/modules/grids/widgets/header/header.component.html
  55. 1
      frontend/src/app/modules/grids/widgets/members/members.component.html
  56. 4
      frontend/src/app/modules/grids/widgets/news/news.component.html
  57. 7
      frontend/src/app/modules/grids/widgets/project-description/project-description.component.html
  58. 3
      frontend/src/app/modules/grids/widgets/project-details/project-details.component.html
  59. 5
      frontend/src/app/modules/grids/widgets/project-status/project-status.component.html
  60. 1
      frontend/src/app/modules/grids/widgets/subprojects/subprojects.component.html
  61. 1
      frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.html
  62. 1
      frontend/src/app/modules/grids/widgets/time-entries/list/time-entries-list.component.html
  63. 1
      frontend/src/app/modules/grids/widgets/wp-calendar/wp-calendar.component.html
  64. 1
      frontend/src/app/modules/grids/widgets/wp-graph/wp-graph.component.html
  65. 3
      frontend/src/app/modules/grids/widgets/wp-overview/wp-overview.component.html
  66. 1
      frontend/src/app/modules/grids/widgets/wp-table/wp-table.component.html
  67. 7
      frontend/src/app/modules/hal/resources/help-text-resource.ts
  68. 19
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts
  69. 4
      frontend/src/app/modules/work_packages/routing/wp-list-view/wp-list-view.component.html
  70. 36
      frontend/src/app/modules/work_packages/routing/wp-list-view/wp-list-view.component.ts
  71. 11
      frontend/src/app/modules/work_packages/routing/wp-view-base/event-handling/event-handler-registry.ts
  72. 9
      lib/api/decorators/schema_representer.rb
  73. 52
      lib/api/v3/attachments/attachments_by_help_text_api.rb
  74. 6
      lib/api/v3/help_texts/help_text_representer.rb
  75. 2
      lib/api/v3/help_texts/help_texts_api.rb
  76. 4
      lib/api/v3/utilities/path_helper.rb
  77. 8
      lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb
  78. 1
      modules/bim/app/seeders/bim/basic_data_seeder.rb
  79. 38
      modules/boards/spec/features/board_navigation_spec.rb
  80. 6
      modules/boards/spec/features/support/board_page.rb
  81. 2
      modules/costs/spec/controllers/costlog_controller_spec.rb
  82. 53
      spec/contracts/attribute_help_texts/base_contract_spec.rb
  83. 4
      spec/controllers/attribute_help_texts_controller_spec.rb
  84. 6
      spec/factories/attribute_help_text_factory.rb
  85. 21
      spec/features/admin/attribute_help_texts_spec.rb
  86. 96
      spec/features/projects/attribute_help_texts_spec.rb
  87. 7
      spec/lib/api/v3/help_texts/help_text_representer_spec.rb
  88. 5
      spec/models/attribute_help_text/work_package_spec.rb
  89. 14
      spec/support/shared/acts_as_attachable.rb

@ -0,0 +1,53 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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 AttributeHelpTexts
class BaseContract < ::ModelContract
include Attachments::ValidateReplacements
def validate
validate_user_allowed_to_manage
super
end
def self.model
AttributeHelpText
end
attribute :type
attribute :attribute_name
attribute :help_text
def validate_user_allowed_to_manage
errors.add :base, :error_unauthorized unless user.admin?
end
end
end

@ -0,0 +1,34 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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 AttributeHelpTexts
class CreateContract < BaseContract
end
end

@ -0,0 +1,34 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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 AttributeHelpTexts
class UpdateContract < BaseContract
end
end

@ -37,6 +37,8 @@ class AttributeHelpTextsController < ApplicationController
before_action :find_type_scope
before_action :require_enterprise_token_grant
helper_method :gon
def new
@attribute_help_text = AttributeHelpText.new type: @attribute_scope
end
@ -44,23 +46,30 @@ class AttributeHelpTextsController < ApplicationController
def edit; end
def update
@attribute_help_text.attributes = permitted_params.attribute_help_text
call = ::AttributeHelpTexts::UpdateService
.new(user: current_user, model: @attribute_help_text)
.call(permitted_params_with_attachments)
if @attribute_help_text.save
if call.success?
flash[:notice] = t(:notice_successful_update)
redirect_to attribute_help_texts_path(tab: @attribute_help_text.attribute_scope)
else
flash[:error] = call.message || I18n.t('notice_internal_server_error')
render action: 'edit'
end
end
def create
@attribute_help_text = AttributeHelpText.new permitted_params.attribute_help_text
call = ::AttributeHelpTexts::CreateService
.new(user: current_user)
.call(permitted_params_with_attachments)
if @attribute_help_text.save
if call.success?
flash[:notice] = t(:notice_successful_create)
redirect_to attribute_help_texts_path(tab: @attribute_help_text.attribute_scope)
redirect_to attribute_help_texts_path(tab: call.result.attribute_scope)
else
@attribute_help_text = call.result
flash[:error] = call.message || I18n.t('notice_internal_server_error')
render action: 'new'
end
end
@ -95,6 +104,20 @@ class AttributeHelpTextsController < ApplicationController
private
def permitted_params_with_attachments
permitted_params.attribute_help_text.merge(attachment_params)
end
def attachment_params
attachment_params = permitted_params.attachments.to_h
if attachment_params.any?
{ attachment_ids: attachment_params.values.map(&:values).flatten }
else
{}
end
end
def find_entry
@attribute_help_text = AttributeHelpText.find(params[:id])
rescue ActiveRecord::RecordNotFound

@ -27,6 +27,8 @@
#++
class AttributeHelpText < ApplicationRecord
acts_as_attachable viewable_by_all_users: true
def self.available_types
subclasses.map { |child| child.name.demodulize }
end
@ -60,7 +62,7 @@ class AttributeHelpText < ApplicationRecord
end
def attribute_scope
raise NotImplementedError
self.class.to_s.demodulize
end
def type_caption
@ -77,3 +79,4 @@ class AttributeHelpText < ApplicationRecord
end
require_dependency 'attribute_help_text/work_package'
require_dependency 'attribute_help_text/project'

@ -0,0 +1,54 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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.
#++
class AttributeHelpText::Project < AttributeHelpText
def self.available_attributes
skip = %w[_type links _dependencies id]
attributes = API::V3::Projects::Schemas::ProjectSchemaRepresenter
.representable_definitions
.reject { |key, _| skip.include?(key.to_s) }
.transform_values { |definition| definition[:name_source].call }
ProjectCustomField.all.each do |field|
attributes["custom_field_#{field.id}"] = field.name
end
attributes
end
validates_inclusion_of :attribute_name, in: ->(*) { available_attributes.keys }
def type_caption
Project.model_name.human
end
def self.visible_condition(_user)
::AttributeHelpText.where(attribute_name: available_attributes.keys)
end
end

@ -43,10 +43,6 @@ class AttributeHelpText::WorkPackage < AttributeHelpText
validates_inclusion_of :attribute_name, in: ->(*) { available_attributes.keys }
def attribute_scope
'WorkPackage'
end
def type_caption
I18n.t(:label_work_package)
end
@ -57,7 +53,8 @@ class AttributeHelpText::WorkPackage < AttributeHelpText
.pluck(:id)
.map { |id| "custom_field_#{id}" }
where(attribute_name: visible_cf_names)
.or(where.not("attribute_name LIKE 'custom_field_%'"))
::AttributeHelpText
.where(attribute_name: visible_cf_names)
.or(::AttributeHelpText.where.not("attribute_name LIKE 'custom_field_%'"))
end
end

@ -27,16 +27,13 @@
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module Bim
module BasicData
class CustomStyleSeeder < Seeder
def seed_data!
CustomStyle.create data
end
def data
{ theme: 'OpenProject Dark' }
end
module AttributeHelpTexts
class CreateService < ::BaseServices::Create
include Attachments::ReplaceAttachments
def instance(params)
instance_class.new type: params[:type]
end
end
end

@ -0,0 +1,35 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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 AttributeHelpTexts
class SetAttributesService < ::BaseServices::SetAttributes
include Attachments::SetReplacements
end
end

@ -0,0 +1,35 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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 AttributeHelpTexts
class UpdateService < ::BaseServices::Update
include Attachments::ReplaceAttachments
end
end

@ -40,7 +40,23 @@ See docs/COPYRIGHT.rdoc for more details.
<%= f.select :attribute_name, selectable_attributes(@attribute_help_text), container_class: '-middle' %>
<% end %>
</div>
<% resource = ::API::V3::HelpTexts::HelpTextRepresenter.new(@attribute_help_text,
current_user: current_user,
embed_links: true) %>
<div class="form--field -required -visible-overflow">
<%= f.text_area :help_text, cols: 100, rows: 20, class: 'wiki-edit', with_text_formatting: true %>
<%= f.text_area :help_text,
cols: 100,
rows: 20,
class: 'wiki-edit',
with_text_formatting: true,
resource: resource %>
<div class="form--field-instructions">
<p>
<strong><%= t(:note) %>:</strong>
<%= t('attribute_help_texts.note_public') %>
</p>
</div>
</div>
</section>

@ -0,0 +1,78 @@
<% entries = @texts_by_type[tab[:name]] || [] %>
<% if entries.any? %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table">
<colgroup>
<col highlight-col>
<col highlight-col>
<col highlight-col>
<col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= AttributeHelpText.human_attribute_name(:attribute_name) %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= AttributeHelpText.human_attribute_name(:help_text) %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--empty-header"></div>
</th>
</tr>
</thead>
<tbody>
<% entries.each do |attribute_help_text| -%>
<tr class="attribute-help-text--entry">
<td>
<%= link_to h(attribute_help_text.attribute_caption),
edit_attribute_help_text_path(attribute_help_text) %>
</td>
<td>
<attribute-help-text
data-help-text-id="<%= attribute_help_text.id %>"
data-attribute="<%= attribute_help_text.attribute_name %>"
data-attribute-scope="'<%= attribute_help_text.attribute_scope %>'"
data-additional-label="<%= t(:'attribute_help_texts.show_preview') %>">
</attribute-help-text>
</td>
<td class="buttons">
<%= link_to(
op_icon('icon icon-delete'),
(attribute_help_text_path(attribute_help_text)),
method: :delete,
data: { confirm: I18n.t(:text_are_you_sure) },
title: t(:button_delete)) %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<%= no_results_box %>
<% end %>
<div class="generic-table--action-buttons">
<%= link_to new_attribute_help_text_path(name: tab[:name]),
{ class: 'attribute-help-texts--create-button button -alt-highlight',
aria: {label: t(:'attribute_help_texts.add_new')},
title: t(:'attribute_help_texts.add_new')} do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= t('activerecord.models.attribute_help_text') %></span>
<% end %>
</div>

@ -26,93 +26,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<% name = 'WorkPackage' %>
<%= toolbar title: t(:'attribute_help_texts.label_plural') do %>
<li class="toolbar-item">
<%= link_to new_attribute_help_text_path(name: name),
{ class: 'attribute-help-texts--create-button button -alt-highlight',
aria: {label: t(:'attribute_help_texts.add_new')},
title: t(:'attribute_help_texts.add_new')} do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= t('activerecord.models.attribute_help_text') %></span>
<% end %>
</li>
<% end %>
<div class="notification-box -info">
<div class="notification-box--content">
<p><%= t('attribute_help_texts.text_overview') %></p>
</div>
</div>
<% html_title(t(:label_administration), t(:'attribute_help_texts.label_plural')) -%>
<% entries = @texts_by_type[name] || [] %>
<% if entries.any? %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table">
<colgroup>
<col highlight-col>
<col highlight-col>
<col highlight-col>
<col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= AttributeHelpText.human_attribute_name(:attribute_name) %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= AttributeHelpText.human_attribute_name(:help_text) %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--empty-header"></div>
</th>
</tr>
</thead>
<tbody>
<% entries.each do |attribute_help_text| -%>
<tr class="attribute-help-text--entry">
<td>
<%= link_to h(attribute_help_text.attribute_caption),
edit_attribute_help_text_path(attribute_help_text) %>
</td>
<td>
<attribute-help-text
data-help-text-id="<%= attribute_help_text.id %>"
data-attribute="<%= attribute_help_text.attribute_name %>"
data-attribute-scope="'<%= attribute_help_text.attribute_scope %>'"
data-additional-label="<%= t(:'attribute_help_texts.show_preview') %>">
</attribute-help-text>
</td>
<td class="buttons">
<%= link_to(
op_icon('icon icon-delete'),
(attribute_help_text_path(attribute_help_text)),
method: :delete,
data: { confirm: I18n.t(:text_are_you_sure) },
title: t(:button_delete)) %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<%= no_results_box %>
<% end %>
<%= render_tabs [
{
name: 'WorkPackage',
partial: 'attribute_help_texts/tab',
path: attribute_help_texts_path(tab: 'WorkPackage'),
label: :label_work_package
},
{
name: 'Project',
partial: 'attribute_help_texts/tab',
path: attribute_help_texts_path(tab: 'Project'),
label: Project.model_name.human
}
]
%>

@ -207,7 +207,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
menu.push :attribute_help_texts,
{ controller: '/attribute_help_texts' },
caption: :'attribute_help_texts.label_plural',
parent: :admin_work_packages,
icon: 'icon2 icon-help2',
if: Proc.new {
EnterpriseToken.allows_to?(:attribute_help_texts)
}

@ -277,7 +277,7 @@ lt:
overview:
no_results_title_text: Šiuo metu nėra šiai versijai priskirtų darbų paketų.
wiki:
page_not_editable_index: The requested page does not (yet) exist. You have been redirected to the index of all wiki pages.
page_not_editable_index: Prašomas puslapis (dar) neegzistuoja. Vietoje to jus nukreipėme į visų wiki puslapių sąrašą.
no_results_title_text: Nėra wiki puslapių.
index:
no_results_content_text: Pridėti naują wiki puslapį

@ -282,7 +282,7 @@ tr:
overview:
no_results_title_text: Bu sürüme atadığım iş paketi bulunmuyor.
wiki:
page_not_editable_index: The requested page does not (yet) exist. You have been redirected to the index of all wiki pages.
page_not_editable_index: İstenen sayfa mevcut değil (henüz). Tüm wiki sayfalarının dizinine yönlendirildiniz.
no_results_title_text: Şu anda wiki sayfası bulunmuyor.
index:
no_results_content_text: Yeni wiki sayfası Ekle

@ -89,6 +89,7 @@ en:
is_inactive: currently not displayed
attribute_help_texts:
note_public: 'Any text and images you add to this field is publically visible to all logged in users!'
text_overview: 'In this view, you can create custom help texts for attributes view. When defined, these texts can be shown by clicking the help icon next to its belonging attribute.'
label_plural: 'Attribute help texts'
show_preview: 'Preview text'

@ -49,29 +49,19 @@ export class CardClickHandler implements CardEventHandler {
return true;
}
this.handleWorkPackage(wpId, element, evt);
this.handleWorkPackage(card, wpId, element, evt);
return false;
}
protected handleWorkPackage(wpId:any, element:JQuery, evt:JQuery.TriggeredEvent) {
this.setSelection(wpId, element, evt);
protected handleWorkPackage(card:WorkPackageCardViewComponent, wpId:any, element:JQuery, evt:JQuery.TriggeredEvent) {
this.setSelection(card, wpId, element, evt);
// open work package on mobile after first click
this.openFullViewOnMobile(wpId);
card.itemClicked.emit({ workPackageId: wpId, double: false });
}
protected openFullViewOnMobile(wpId:string) {
if (this.deviceService.isMobile) {
this.$state.go(
'work-packages.show',
{workPackageId: wpId}
);
}
}
protected setSelection(wpId:string, element:JQuery, evt:JQuery.TriggeredEvent) {
protected setSelection(card:WorkPackageCardViewComponent, wpId:string, element:JQuery, evt:JQuery.TriggeredEvent) {
let classIdentifier = element.data('classIdentifier');
let index = this.wpCardView.findRenderedCard(classIdentifier);
@ -90,6 +80,8 @@ export class CardClickHandler implements CardEventHandler {
this.wpTableSelection.toggleRow(wpId);
}
card.selectionChanged.emit(this.wpTableSelection.getSelectedWorkPackageIds());
// The current card is the last selected work package
// not matter what other card are (de-)selected below.
// Thus save that card for the details view button.

@ -41,16 +41,8 @@ export class CardDblClickHandler implements CardEventHandler {
return true;
}
this.handleWorkPackage(wpId);
card.itemClicked.emit({ workPackageId: wpId, double: true });
return false;
}
protected handleWorkPackage(wpId:string) {
this.$state.go(
'work-packages.show',
{workPackageId: wpId}
);
}
}

@ -33,7 +33,10 @@ import {WorkPackageCardViewService} from "core-components/wp-card-view/services/
import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service";
import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service";
import {DeviceService} from "core-app/modules/common/browser/device.service";
import {WorkPackageViewHandlerToken} from "core-app/modules/work_packages/routing/wp-view-base/event-handling/event-handler-registry";
import {
WorkPackageViewHandlerToken,
WorkPackageViewOutputs
} from "core-app/modules/work_packages/routing/wp-view-base/event-handling/event-handler-registry";
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin";
import {componentDestroyed} from "@w11k/ngx-componentdestroyed";
import {HalEventsService} from "core-app/modules/hal/services/hal-events.service";
@ -46,7 +49,7 @@ export type CardViewOrientation = 'horizontal'|'vertical';
templateUrl: './wp-card-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WorkPackageCardViewComponent extends UntilDestroyedMixin implements OnInit, AfterViewInit {
export class WorkPackageCardViewComponent extends UntilDestroyedMixin implements OnInit, AfterViewInit, WorkPackageViewOutputs {
@Input('dragOutOfHandler') public canDragOutOf:(wp:WorkPackageResource) => boolean;
@Input() public dragInto:boolean;
@Input() public highlightingMode:CardHighlightingMode;
@ -65,6 +68,9 @@ export class WorkPackageCardViewComponent extends UntilDestroyedMixin implements
@ViewChild('container', { static: true }) public container:ElementRef;
@Output() public onMoved = new EventEmitter<void>();
@Output() selectionChanged = new EventEmitter<string[]>();
@Output() itemClicked = new EventEmitter<{ workPackageId:string, double:boolean }>();
@Output() stateLinkClicked = new EventEmitter<{ workPackageId:string, requestedState:string }>();
public trackByHref = AngularTrackingHelpers.trackByHrefAndProperty('lockVersion');
public query:QueryResource;

@ -4,6 +4,9 @@ import {StateService} from '@uirouter/core';
export const uiStateLinkClass = '__ui-state-link';
export const checkedClassName = '-checked';
export const currentDetailsState = 'currentDetailsState';
export const currentShowState = 'currentShowState';
export class UiStateLinkBuilder {
constructor(public readonly $state:StateService,
@ -11,11 +14,11 @@ export class UiStateLinkBuilder {
}
public linkToDetails(workPackageId:string, title:string, content:string) {
return this.build(workPackageId, 'currentDetailsState', title, content);
return this.build(workPackageId, currentDetailsState, title, content);
}
public linkToShow(workPackageId:string, title:string, content:string) {
return this.build(workPackageId, 'currentShowState', title, content);
return this.build(workPackageId, currentShowState, title, content);
}
private build(workPackageId:string, state:string, title:string, content:string) {

@ -7,7 +7,7 @@ import {HalResourceEditingService} from "core-app/modules/fields/edit/services/h
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {ClickOrEnterHandler} from '../click-or-enter-handler';
import {TableEventHandler} from '../table-handler-registry';
import {TableEventComponent, TableEventHandler} from '../table-handler-registry';
import {ClickPositionMapper} from "core-app/modules/common/set-click-position/set-click-position";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@ -27,11 +27,11 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa
return `.${displayClassName}.${editableClassName}`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tableAndTimelineContainer);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tableAndTimelineContainer);
}
constructor(public readonly injector:Injector, table:WorkPackageTable) {
constructor(public readonly injector:Injector) {
super();
}

@ -4,7 +4,7 @@ import {relationCellIndicatorClassName, relationCellTdClassName} from '../../bui
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {ClickOrEnterHandler} from '../click-or-enter-handler';
import {TableEventHandler} from '../table-handler-registry';
import {TableEventComponent, TableEventHandler} from '../table-handler-registry';
import {WorkPackageViewRelationColumnsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-relation-columns.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@ -21,12 +21,11 @@ export class RelationsCellHandler extends ClickOrEnterHandler implements TableEv
return `.${relationCellIndicatorClassName}`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tableAndTimelineContainer);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tableAndTimelineContainer);
}
constructor(public readonly injector:Injector,
table:WorkPackageTable) {
constructor(public readonly injector:Injector) {
super();
}

@ -1,5 +1,6 @@
import {keyCodes} from 'core-app/modules/common/keyCodes.enum';
import {WorkPackageTable} from "../wp-fast-table";
import {TableEventComponent} from "core-components/wp-fast-table/handlers/table-handler-registry";
/**
@ -16,8 +17,8 @@ export function onClickOrEnter(evt:JQuery.TriggeredEvent, callback:() => void) {
export abstract class ClickOrEnterHandler {
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent) {
onClickOrEnter(evt, () => this.processEvent(table, evt));
public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
onClickOrEnter(evt, () => this.processEvent(view.workPackageTable, evt));
}
protected abstract processEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):boolean;

@ -4,12 +4,12 @@ import {uiStateLinkClass} from '../../builders/ui-state-link-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {ContextMenuHandler} from './context-menu-handler';
import {contextMenuLinkClassName} from "core-components/wp-table/table-actions/table-action";
import {TableEventComponent} from "core-components/wp-fast-table/handlers/table-handler-registry";
export class ContextMenuClickHandler extends ContextMenuHandler {
constructor(public readonly injector:Injector,
table:WorkPackageTable) {
super(injector, table);
constructor(public readonly injector:Injector) {
super(injector);
}
public get EVENT() {
@ -20,7 +20,7 @@ export class ContextMenuClickHandler extends ContextMenuHandler {
return `.${contextMenuLinkClassName}`;
}
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):boolean {
public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent):boolean {
let target = jQuery(evt.target);
// We want to keep the original context menu on hrefs
@ -38,7 +38,7 @@ export class ContextMenuClickHandler extends ContextMenuHandler {
const wpId = element.data('workPackageId');
if (wpId) {
super.openContextMenu(evt, wpId);
this.openContextMenu(view.workPackageTable, evt, wpId);
}
return false;

@ -1,7 +1,7 @@
import {Injector} from '@angular/core';
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {TableEventHandler} from '../table-handler-registry';
import {TableEventComponent, TableEventHandler} from '../table-handler-registry';
import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service";
import {WorkPackageTableContextMenu} from "core-components/op-context-menu/wp-context-menu/wp-table-context-menu.directive";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@ -10,8 +10,7 @@ export abstract class ContextMenuHandler implements TableEventHandler {
// Injections
@InjectField() public opContextMenu:OPContextMenuService;
constructor(public readonly injector:Injector,
protected table:WorkPackageTable) {
constructor(public readonly injector:Injector) {
}
public get rowSelector() {
@ -22,14 +21,14 @@ export abstract class ContextMenuHandler implements TableEventHandler {
public abstract get SELECTOR():string;
public eventScope(table:WorkPackageTable) {
return jQuery(table.tableAndTimelineContainer);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tableAndTimelineContainer);
}
public abstract handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):boolean;
public abstract handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent):boolean;
protected openContextMenu(evt:JQuery.TriggeredEvent, workPackageId:string, positionArgs?:any):void {
const handler = new WorkPackageTableContextMenu(this.injector, workPackageId, jQuery(evt.target) as JQuery, positionArgs, this.table);
protected openContextMenu(table:WorkPackageTable, evt:JQuery.TriggeredEvent, workPackageId:string, positionArgs?:any):void {
const handler = new WorkPackageTableContextMenu(this.injector, workPackageId, jQuery(evt.target) as JQuery, positionArgs, table);
this.opContextMenu.show(handler, evt);
}
}

@ -2,12 +2,12 @@ import {Injector} from '@angular/core';
import {keyCodes} from 'core-app/modules/common/keyCodes.enum';
import {WorkPackageTable} from '../../wp-fast-table';
import {ContextMenuHandler} from './context-menu-handler';
import {TableEventComponent} from "core-components/wp-fast-table/handlers/table-handler-registry";
export class ContextMenuKeyboardHandler extends ContextMenuHandler {
constructor(public readonly injector:Injector,
table:WorkPackageTable) {
super(injector, table);
constructor(public readonly injector:Injector) {
super(injector);
}
public get EVENT() {
@ -18,8 +18,8 @@ export class ContextMenuKeyboardHandler extends ContextMenuHandler {
return this.rowSelector;
}
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):boolean {
if (!table.configuration.contextMenuEnabled) {
public handleEvent(component:TableEventComponent, evt:JQuery.TriggeredEvent):boolean {
if (!component.workPackageTable.configuration.contextMenuEnabled) {
return false;
}
@ -39,7 +39,7 @@ export class ContextMenuKeyboardHandler extends ContextMenuHandler {
// Set position args to open at element
let position = { my: 'left top', at: 'left bottom', of: target };
super.openContextMenu(evt, wpId, position);
super.openContextMenu(component.workPackageTable, evt, wpId, position);
return false;
}

@ -7,15 +7,14 @@ import {WorkPackageTable} from '../../wp-fast-table';
import {ContextMenuHandler} from './context-menu-handler';
import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {TableEventComponent} from "core-components/wp-fast-table/handlers/table-handler-registry";
export class ContextMenuRightClickHandler extends ContextMenuHandler {
@InjectField() readonly wpTableSelection:WorkPackageViewSelectionService;
constructor(public readonly injector:Injector,
table:WorkPackageTable) {
super(injector, table);
constructor(public readonly injector:Injector) {
super(injector);
}
public get EVENT() {
@ -26,12 +25,12 @@ export class ContextMenuRightClickHandler extends ContextMenuHandler {
return `.${tableRowClassName},.${timelineCellClassName}`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tableAndTimelineContainer);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tableAndTimelineContainer);
}
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):boolean {
if (!table.configuration.contextMenuEnabled) {
public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent):boolean {
if (!view.workPackageTable.configuration.contextMenuEnabled) {
return false;
}
let target = jQuery(evt.target);
@ -51,13 +50,13 @@ export class ContextMenuRightClickHandler extends ContextMenuHandler {
const wpId = element.data('workPackageId');
if (wpId) {
let [index,] = this.table.findRenderedRow(wpId);
let [index,] = view.workPackageTable.findRenderedRow(wpId);
if (!this.wpTableSelection.isSelected(wpId)) {
this.wpTableSelection.setSelection(wpId, index);
}
super.openContextMenu(evt, wpId);
this.openContextMenu(view.workPackageTable, evt, wpId);
}
return false;

@ -6,7 +6,7 @@ import {States} from '../../../states.service';
import {KeepTabService} from '../../../wp-single-view-tabs/keep-tab/keep-tab.service';
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {TableEventHandler} from '../table-handler-registry';
import {TableEventComponent, TableEventHandler} from '../table-handler-registry';
import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service";
import {displayClassName} from "core-components/wp-edit-form/display-field-renderer";
import {activeFieldClassName} from "core-app/modules/fields/edit/edit-form/edit-form";
@ -21,8 +21,7 @@ export class RowClickHandler implements TableEventHandler {
@InjectField() public wpTableSelection:WorkPackageViewSelectionService;
@InjectField() public wpTableFocus:WorkPackageViewFocusService;
constructor(public readonly injector:Injector,
table:WorkPackageTable) {
constructor(public readonly injector:Injector) {
}
public get EVENT() {
@ -33,11 +32,11 @@ export class RowClickHandler implements TableEventHandler {
return `.${tableRowClassName}`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tbody);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tbody);
}
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent) {
public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
let target = jQuery(evt.target);
// Ignore links
@ -61,16 +60,17 @@ export class RowClickHandler implements TableEventHandler {
return true;
}
let [index, row] = table.findRenderedRow(classIdentifier);
let [index, row] = view.workPackageTable.findRenderedRow(classIdentifier);
// Update single selection if no modifier present
if (!(evt.ctrlKey || evt.metaKey || evt.shiftKey)) {
this.wpTableSelection.setSelection(wpId, index);
view.itemClicked.emit({ workPackageId: wpId, double: false });
}
// Multiple selection if shift present
if (evt.shiftKey) {
this.wpTableSelection.setMultiSelectionFrom(table.renderedRows, wpId, index);
this.wpTableSelection.setMultiSelectionFrom(view.workPackageTable.renderedRows, wpId, index);
}
// Single selection expansion if ctrl / cmd(mac)
@ -78,6 +78,8 @@ export class RowClickHandler implements TableEventHandler {
this.wpTableSelection.toggleRow(wpId);
}
view.selectionChanged.emit(this.wpTableSelection.getSelectedWorkPackageIds());
// The current row is the last selected work package
// not matter what other rows are (de-)selected below.
// Thus save that row for the details view button.

@ -6,7 +6,7 @@ import {States} from '../../../states.service';
import {tdClassName} from '../../builders/cell-builder';
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {TableEventHandler} from '../table-handler-registry';
import {TableEventComponent, TableEventHandler} from '../table-handler-registry';
import {LinkHandling} from "core-app/modules/common/link-handling/link-handling";
import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service";
import {displayClassName} from "core-components/wp-edit-form/display-field-renderer";
@ -21,8 +21,7 @@ export class RowDoubleClickHandler implements TableEventHandler {
@InjectField() public wpTableSelection:WorkPackageViewSelectionService;
@InjectField() public wpTableFocus:WorkPackageViewFocusService;
constructor(public readonly injector:Injector,
table:WorkPackageTable) {
constructor(public readonly injector:Injector) {
}
public get EVENT() {
@ -33,11 +32,11 @@ export class RowDoubleClickHandler implements TableEventHandler {
return `.${tdClassName}`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tbody);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tbody);
}
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent) {
public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
let target = jQuery(evt.target);
// Skip clicks with modifiers
@ -64,10 +63,7 @@ export class RowDoubleClickHandler implements TableEventHandler {
// Save the currently focused work package
this.wpTableFocus.updateFocus(wpId);
this.$state.go(
'work-packages.show',
{workPackageId: wpId}
);
view.itemClicked.emit({ workPackageId: wpId, double: true });
return false;
}

@ -2,7 +2,7 @@ import {Injector} from '@angular/core';
import {debugLog} from '../../../../helpers/debug_output';
import {GroupedRowsBuilder} from '../../builders/modes/grouped/grouped-rows-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {TableEventHandler} from '../table-handler-registry';
import {TableEventComponent, TableEventHandler} from '../table-handler-registry';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {rowGroupClassName} from "core-components/wp-fast-table/builders/modes/grouped/grouped-classes.constants";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@ -12,10 +12,7 @@ export class GroupRowHandler implements TableEventHandler {
// Injections
@InjectField() public querySpace:IsolatedQuerySpace;
private builder:GroupedRowsBuilder;
constructor(public readonly injector:Injector, table:WorkPackageTable) {
this.builder = new GroupedRowsBuilder(injector, table);
constructor(public readonly injector:Injector) {
}
public get EVENT() {
@ -26,11 +23,11 @@ export class GroupRowHandler implements TableEventHandler {
return `.${rowGroupClassName} .expander`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tbody);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tbody);
}
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent) {
public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
evt.preventDefault();
evt.stopPropagation();
@ -42,9 +39,10 @@ export class GroupRowHandler implements TableEventHandler {
this.collapsedState.putValue(state);
// Refresh groups
var t0 = performance.now();
this.builder.refreshExpansionState();
var t1 = performance.now();
const builder = new GroupedRowsBuilder(this.injector, view.workPackageTable);
const t0 = performance.now();
builder.refreshExpansionState();
const t1 = performance.now();
debugLog('Group redraw took ' + (t1 - t0) + ' milliseconds.');
}

@ -3,7 +3,7 @@ import {States} from '../../../states.service';
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {ClickOrEnterHandler} from '../click-or-enter-handler';
import {TableEventHandler} from "core-components/wp-fast-table/handlers/table-handler-registry";
import {TableEventComponent, TableEventHandler} from "core-components/wp-fast-table/handlers/table-handler-registry";
import {WorkPackageViewHierarchiesService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-hierarchy.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@ -12,7 +12,7 @@ export class HierarchyClickHandler extends ClickOrEnterHandler implements TableE
@InjectField() public states:States;
@InjectField() public wpTableHierarchies:WorkPackageViewHierarchiesService;
constructor(public readonly injector:Injector, table:WorkPackageTable) {
constructor(public readonly injector:Injector) {
super();
}
@ -24,8 +24,8 @@ export class HierarchyClickHandler extends ClickOrEnterHandler implements TableE
return `.wp-table--hierarchy-indicator`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tbody);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tbody);
}
public processEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):boolean {

@ -6,7 +6,7 @@ import {KeepTabService} from '../../../wp-single-view-tabs/keep-tab/keep-tab.ser
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {uiStateLinkClass} from '../../builders/ui-state-link-builder';
import {WorkPackageTable} from '../../wp-fast-table';
import {TableEventHandler} from '../table-handler-registry';
import {TableEventComponent, TableEventHandler} from '../table-handler-registry';
import {StateService} from '@uirouter/core';
import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@ -20,8 +20,7 @@ export class WorkPackageStateLinksHandler implements TableEventHandler {
@InjectField() public wpTableSelection:WorkPackageViewSelectionService;
@InjectField() public wpTableFocus:WorkPackageViewFocusService;
constructor(public readonly injector:Injector,
table:WorkPackageTable) {
constructor(public readonly injector:Injector) {
}
public get EVENT() {
@ -32,13 +31,13 @@ export class WorkPackageStateLinksHandler implements TableEventHandler {
return `.${uiStateLinkClass}`;
}
public eventScope(table:WorkPackageTable) {
return jQuery(table.tableAndTimelineContainer);
public eventScope(view:TableEventComponent) {
return jQuery(view.workPackageTable.tableAndTimelineContainer);
}
protected workPackage:WorkPackageResource;
public handleEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent) {
public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
// Avoid the state capture when clicking with modifier
if (evt.shiftKey || evt.ctrlKey || evt.metaKey || evt.altKey) {
return true;
@ -59,15 +58,12 @@ export class WorkPackageStateLinksHandler implements TableEventHandler {
// Locate the row from event
let row = target.closest(`.${tableRowClassName}`);
let classIdentifier = row.data('classIdentifier');
let [index, _] = table.findRenderedRow(classIdentifier);
let [index, _] = view.workPackageTable.findRenderedRow(classIdentifier);
// Update single selection if no modifier present
this.wpTableSelection.setSelection(workPackageId, index);
this.$state.go(
(this.keepTab as any)[state],
{workPackageId: workPackageId, focus: true}
);
view.stateLinkClicked.emit({ workPackageId: workPackageId, requestedState: state });
evt.preventDefault();
evt.stopPropagation();

@ -1,4 +1,4 @@
import {Injector} from '@angular/core';
import {EventEmitter, Injector} from '@angular/core';
import {WorkPackageTable} from '../wp-fast-table';
import {EditCellHandler} from './cell/edit-cell-handler';
import {RelationsCellHandler} from './cell/relations-cell-handler';
@ -19,40 +19,46 @@ import {TimelineTransformer} from './state/timeline-transformer';
import {HighlightingTransformer} from "core-components/wp-fast-table/handlers/state/highlighting-transformer";
import {DragAndDropTransformer} from "core-components/wp-fast-table/handlers/state/drag-and-drop-transformer";
import {
WorkPackageViewEventHandler,
WorkPackageViewEventHandler, WorkPackageViewOutputs,
WorkPackageViewHandlerRegistry
} from "core-app/modules/work_packages/routing/wp-view-base/event-handling/event-handler-registry";
import {WorkPackageFocusContext} from "core-components/wp-table/wp-table.component";
type StateTransformers = {
// noinspection JSUnusedLocalSymbols
new(injector:Injector, table:WorkPackageTable):any;
};
export type TableEventHandler = WorkPackageViewEventHandler<WorkPackageTable>;
export interface TableEventComponent extends WorkPackageViewOutputs {
// Reference to the fast table instance
workPackageTable:WorkPackageTable;
}
export type TableEventHandler = WorkPackageViewEventHandler<TableEventComponent>;
export class TableHandlerRegistry extends WorkPackageViewHandlerRegistry<WorkPackageTable> {
export class TableHandlerRegistry extends WorkPackageViewHandlerRegistry<TableEventComponent> {
protected eventHandlers:((t:WorkPackageTable) => WorkPackageViewEventHandler<WorkPackageTable>)[] = [
protected eventHandlers:((t:TableEventComponent) => TableEventHandler)[] = [
// Hierarchy expansion/collapsing
t => new HierarchyClickHandler(this.injector, t),
() => new HierarchyClickHandler(this.injector),
// Clicking or pressing Enter on a single cell, editable or not
t => new EditCellHandler(this.injector, t),
() => new EditCellHandler(this.injector),
// Clicking on the details view
t => new WorkPackageStateLinksHandler(this.injector, t),
() => new WorkPackageStateLinksHandler(this.injector),
// Clicking on the row (not within a cell)
t => new RowClickHandler(this.injector, t),
() => new RowClickHandler(this.injector),
// Double Clicking on the cell within the row
t => new RowDoubleClickHandler(this.injector, t),
() => new RowDoubleClickHandler(this.injector),
// Clicking on group headers
t => new GroupRowHandler(this.injector, t),
() => new GroupRowHandler(this.injector),
// Right clicking on rows
t => new ContextMenuRightClickHandler(this.injector, t),
() => new ContextMenuRightClickHandler(this.injector),
// Left clicking on the dropdown icon
t => new ContextMenuClickHandler(this.injector, t),
() => new ContextMenuClickHandler(this.injector),
// SHIFT+ALT+F10 on rows
t => new ContextMenuKeyboardHandler(this.injector, t),
() => new ContextMenuKeyboardHandler(this.injector),
// Clicking on relations cells
t => new RelationsCellHandler(this.injector, t)
() => new RelationsCellHandler(this.injector)
];
protected readonly stateTransformers:StateTransformers[] = [
@ -66,9 +72,9 @@ export class TableHandlerRegistry extends WorkPackageViewHandlerRegistry<WorkPac
DragAndDropTransformer
];
attachTo(viewRef:WorkPackageTable) {
attachTo(viewRef:TableEventComponent) {
this.stateTransformers.map((cls) => {
return new cls(this.injector, viewRef);
return new cls(this.injector, viewRef.workPackageTable);
});
super.attachTo(viewRef);

@ -26,7 +26,7 @@
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input} from "@angular/core";
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output} from "@angular/core";
import {WorkPackageViewHighlightingService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-highlighting.service";
import {CardViewOrientation} from "core-components/wp-card-view/wp-card-view.component";
import {WorkPackageViewSortByService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-sort-by.service";
@ -37,6 +37,7 @@ import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and
import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service";
import {WorkPackagesListService} from "core-components/wp-list/wp-list.service";
import {WorkPackageTableConfiguration} from "core-components/wp-table/wp-table-configuration";
import {WorkPackageViewOutputs} from "core-app/modules/work_packages/routing/wp-view-base/event-handling/event-handler-registry";
@Component({
selector: 'wp-grid',
@ -48,6 +49,9 @@ import {WorkPackageTableConfiguration} from "core-components/wp-table/wp-table-c
[showStatusButton]="true"
[orientation]="gridOrientation"
(onMoved)="switchToManualSorting()"
(selectionChanged)="selectionChanged.emit($event)"
(itemClicked)="itemClicked.emit($event)"
(stateLinkClicked)="stateLinkClicked.emit($event)"
[showEmptyResultsBox]="true"
[showInfoButton]="true"
[shrinkOnMobile]="true">
@ -65,12 +69,16 @@ import {WorkPackageTableConfiguration} from "core-components/wp-table/wp-table-c
WorkPackageCardDragAndDropService
]
})
export class WorkPackagesGridComponent {
export class WorkPackagesGridComponent implements WorkPackageViewOutputs {
@Input() public configuration:WorkPackageTableConfiguration;
@Input() public showResizer:boolean = false;
@Input() public resizerClass:string = '';
@Input() public resizerStorageKey:string = '';
@Output() selectionChanged = new EventEmitter<string[]>();
@Output() itemClicked = new EventEmitter<{ workPackageId:string, double:boolean }>();
@Output() stateLinkClicked = new EventEmitter<{ workPackageId:string, requestedState:string }>();
public canDragOutOf:() => boolean;
public dragInto:boolean;
public gridOrientation:CardViewOrientation = 'horizontal';

@ -12,6 +12,7 @@ import {QueryFormResource} from "core-app/modules/hal/resources/query-form-resou
import {QueryFormDmService} from "core-app/modules/hal/dm-services/query-form-dm.service";
import {distinctUntilChanged, map, take, withLatestFrom} from "rxjs/operators";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {KeepTabService} from "core-components/wp-single-view-tabs/keep-tab/keep-tab.service";
@Component({
selector: 'wp-embedded-table',
@ -35,6 +36,7 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo
@InjectField() wpTableTimeline:WorkPackageViewTimelineService;
@InjectField() wpTablePagination:WorkPackageViewPaginationService;
@InjectField() QueryFormDm:QueryFormDmService;
@InjectField() keepTab:KeepTabService;
// Cache the form promise
private formPromise:Promise<QueryFormResource>|undefined;
@ -168,4 +170,20 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo
return promise;
}
handleWorkPackageClicked(event:{ workPackageId:string; double:boolean }) {
if (event.double) {
this.$state.go(
'work-packages.show',
{ workPackageId: event.workPackageId }
);
}
}
openStateLink(event:{ workPackageId:string; requestedState:string }) {
this.$state.go(
(this.keepTab as any)[event.requestedState] || event.requestedState,
{ workPackageId: event.workPackageId, focus: true }
);
}
}

@ -17,11 +17,15 @@
<wp-table *ngIf="!configuration.isCardView"
[projectIdentifier]="projectIdentifier"
[configuration]="configuration"
(itemClicked)="handleWorkPackageClicked($event)"
(stateLinkClicked)="openStateLink($event)"
class="work-packages-split-view--tabletimeline-content"></wp-table>
<!-- GRID representation of the WP -->
<div class="work-packages-embedded-view--grid-view" >
<wp-grid *ngIf="configuration.isCardView"
(itemClicked)="handleWorkPackageClicked($event)"
(stateLinkClicked)="openStateLink($event)"
[configuration]="configuration">
</wp-grid>
</div>

@ -48,7 +48,6 @@ import {input, InputState} from "reactivestates";
import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table";
import {WorkPackageTimelineCellsRenderer} from "core-components/wp-table/timeline/cells/wp-timeline-cells-renderer";
import {States} from "core-components/states.service";
import {WorkPackagesTableController} from "core-components/wp-table/wp-table.directive";
import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service";
import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service";
import {WorkPackageViewHierarchiesService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-hierarchy.service";
@ -60,6 +59,7 @@ import {HalEventsService} from "core-app/modules/hal/services/hal-events.service
import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service";
import {combineLatest} from "rxjs";
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin";
import {WorkPackagesTableComponent} from "core-components/wp-table/wp-table.component";
@Component({
selector: 'wp-timeline-container',
@ -95,7 +95,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
constructor(public readonly injector:Injector,
private elementRef:ElementRef,
private states:States,
public wpTableDirective:WorkPackagesTableController,
public wpTableComponent:WorkPackagesTableComponent,
private NotificationsService:NotificationsService,
private wpTableTimeline:WorkPackageViewTimelineService,
private notificationService:WorkPackageNotificationService,
@ -119,7 +119,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
this.timelineBody = this.$element.find('.wp-table-timeline--body');
// Register this instance to the table
this.wpTableDirective.registerTimeline(this, this.timelineBody[0]);
this.wpTableComponent.registerTimeline(this, this.timelineBody[0]);
// Refresh on window resize events
window.addEventListener('wp-resize.timeline', () => this.refreshRequest.putValue(undefined));
@ -408,7 +408,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
// did the zoom level changed?
if (previousZoomLevel !== zoomLevel) {
this._viewParameters.settings.zoomLevel = zoomLevel;
this.wpTableDirective.timeline.scrollLeft = 0;
this.wpTableComponent.timeline.scrollLeft = 0;
}
this.wpTableTimeline.appliedZoomLevel = zoomLevel;

@ -30,17 +30,17 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
ElementRef, EventEmitter,
Injector,
Input,
NgZone,
OnInit,
OnInit, Output,
ViewEncapsulation
} from '@angular/core';
import {QueryGroupByResource} from 'core-app/modules/hal/resources/query-group-by-resource';
import {QueryResource} from 'core-app/modules/hal/resources/query-resource';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {TableHandlerRegistry} from 'core-components/wp-fast-table/handlers/table-handler-registry';
import {TableEventComponent, TableHandlerRegistry} from 'core-components/wp-fast-table/handlers/table-handler-registry';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {combineLatest} from 'rxjs';
import {States} from '../states.service';
@ -61,6 +61,13 @@ import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table";
import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service";
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin";
export interface WorkPackageFocusContext {
/** Work package that was focused */
workPackageId:string;
/** Through what action did the focus happen */
through:'row-double-click'|'id-click'|'details-icon';
}
@Component({
templateUrl: './wp-table.directive.html',
styleUrls: ['./wp-table.styles.sass'],
@ -68,11 +75,15 @@ import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixi
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wp-table',
})
export class WorkPackagesTableController extends UntilDestroyedMixin implements OnInit {
export class WorkPackagesTableComponent extends UntilDestroyedMixin implements OnInit, TableEventComponent {
@Input() projectIdentifier:string;
@Input('configuration') configurationObject:WorkPackageTableConfigurationObject;
@Output() selectionChanged = new EventEmitter<string[]>();
@Output() itemClicked = new EventEmitter<{ workPackageId:string, double:boolean }>();
@Output() stateLinkClicked = new EventEmitter<{ workPackageId:string, requestedState:string }>();
public trackByHref = AngularTrackingHelpers.trackByHref;
public configuration:WorkPackageTableConfiguration;
@ -210,7 +221,7 @@ export class WorkPackagesTableController extends UntilDestroyedMixin implements
);
this.tbody = tbody;
controller.workPackageTable = this.workPackageTable;
new TableHandlerRegistry(this.injector).attachTo(this.workPackageTable);
new TableHandlerRegistry(this.injector).attachTo(this);
// Locate table and timeline elements
const tableAndTimeline = this.getTableAndTimelineElement();

@ -1,25 +1,25 @@
import {ChangeDetectionStrategy, Component, OnInit} from "@angular/core";
import {WorkPackageViewHandlerToken} from "core-app/modules/work_packages/routing/wp-view-base/event-handling/event-handler-registry";
import {BcfCardViewHandlerRegistry} from "core-app/modules/bim/ifc_models/ifc-base-view/event-handler/bcf-card-view-handler-registry";
import {WorkPackageListViewComponent} from "core-app/modules/work_packages/routing/wp-list-view/wp-list-view.component";
import {QueryResource} from "core-app/modules/hal/resources/query-resource";
import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service";
import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service";
import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and-drop.service";
import {CausedUpdatesService} from "core-app/modules/boards/board/caused-updates/caused-updates.service";
import {bimSplitViewCardsIdentifier, bimSplitViewListIdentifier, BimViewService} from "core-app/modules/bim/ifc_models/pages/viewer/bim-view.service";
import {bimSplitViewCardsIdentifier, bimSplitViewListIdentifier, bimListViewIdentifier, BimViewService} from "core-app/modules/bim/ifc_models/pages/viewer/bim-view.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {wpDisplayCardRepresentation, wpDisplayListRepresentation} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service";
import {IfcModelsDataService} from "core-app/modules/bim/ifc_models/pages/viewer/ifc-models-data.service";
import {WorkPackageViewColumnsService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-columns.service';
import {UIRouterGlobals, UIRouter, TransitionService} from '@uirouter/core';
import {pluck, distinctUntilChanged, filter} from "rxjs/operators";
import {UIRouterGlobals} from '@uirouter/core';
import {pluck, distinctUntilChanged} from "rxjs/operators";
import {IFCViewerService} from "core-app/modules/bim/ifc_models/ifc-viewer/ifc-viewer.service";
import {States} from "core-components/states.service";
import {BcfApiService} from "core-app/modules/bim/bcf/api/bcf-api.service";
import {splitViewRoute} from "core-app/modules/work_packages/routing/split-view-routes.helper";
@Component({
templateUrl: '/app/modules/bim/ifc_models/bcf/list-container/bfc-list-container.component.html',
styleUrls: ['/app/modules/bim/ifc_models/bcf/list-container/bcf-list-container.component.sass'],
providers: [
{ provide: WorkPackageViewHandlerToken, useValue: BcfCardViewHandlerRegistry },
{ provide: HalResourceNotificationService, useClass: WorkPackageNotificationService },
DragAndDropService,
CausedUpdatesService
@ -31,6 +31,9 @@ export class BcfListContainerComponent extends WorkPackageListViewComponent impl
@InjectField() ifcModelsService:IfcModelsDataService;
@InjectField() wpTableColumns:WorkPackageViewColumnsService;
@InjectField() uIRouterGlobals:UIRouterGlobals;
@InjectField() viewer:IFCViewerService;
@InjectField() states:States;
@InjectField() bcfApi:BcfApiService;
public wpTableConfiguration = {
dragAndDropEnabled: false
@ -76,4 +79,34 @@ export class BcfListContainerComponent extends WorkPackageListViewComponent impl
this.bimView.currentViewerState() === bimSplitViewListIdentifier;
}
}
handleWorkPackageClicked(event:{ workPackageId:string; double:boolean }) {
// Open the viewpoint if any
const wp = this.states.workPackages.get(event.workPackageId).value;
if (wp && this.viewer.viewerVisible() && wp.bcfViewpoints) {
this.viewer.showViewpoint(wp, 0);
}
if (event.double) {
this.$state.go(
splitViewRoute(this.$state),
{ workPackageId: event.workPackageId }
);
}
}
openStateLink(event:{ workPackageId:string; requestedState:string }) {
// In case we're in a regular list without view,
// reuse the default list behavior
if (this.bimView.current === bimListViewIdentifier) {
super.openStateLink(event);
return;
}
// Otherwise, always open all links in the details view
this.$state.go(
splitViewRoute(this.$state),
{ workPackageId: event.workPackageId, focus: true }
);
}
}

@ -11,6 +11,8 @@
<wp-table [projectIdentifier]="CurrentProject.identifier"
[configuration]="wpTableConfiguration"
(itemClicked)="handleWorkPackageClicked($event)"
(stateLinkClicked)="openStateLink($event)"
class="work-packages-split-view--tabletimeline-content">
</wp-table>
</div>
@ -21,6 +23,8 @@
[ngClass]="{ '-with-resizer': showResizerInCardView() }" >
<wp-grid [configuration]="wpTableConfiguration"
[showResizer]="showResizerInCardView()"
(itemClicked)="handleWorkPackageCardClicked($event)"
(stateLinkClicked)="openStateLink($event)"
resizerClass="work-packages-partitioned-page--content-right"
resizerStorageKey="openProject-splitViewFlexBasis">
</wp-grid>

@ -1,22 +0,0 @@
import {WorkPackageCardViewComponent} from "core-components/wp-card-view/wp-card-view.component";
import {
CardEventHandler,
CardViewHandlerRegistry
} from "core-components/wp-card-view/event-handler/card-view-handler-registry";
import {BcfDoubleClickHandler} from "core-app/modules/bim/ifc_models/ifc-base-view/event-handler/bcf-double-click-handler";
import {BcfClickHandler} from "core-app/modules/bim/ifc_models/ifc-base-view/event-handler/bcf-click-handler";
import {CardRightClickHandler} from "core-components/wp-card-view/event-handler/right-click-handler";
export class BcfCardViewHandlerRegistry extends CardViewHandlerRegistry {
protected eventHandlers:((c:WorkPackageCardViewComponent) => CardEventHandler)[] = [
// Clicking on the card (not within a cell)
c => new BcfClickHandler(this.injector, c),
// Double clicking on the card
c => new BcfDoubleClickHandler(this.injector, c),
// Right clicking on cards
t => new CardRightClickHandler(this.injector, t),
];
}

@ -1,22 +0,0 @@
import {CardClickHandler} from "core-components/wp-card-view/event-handler/click-handler";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {States} from "core-components/states.service";
import {IFCViewerService} from "core-app/modules/bim/ifc_models/ifc-viewer/ifc-viewer.service";
import {BcfApiService} from "core-app/modules/bim/bcf/api/bcf-api.service";
import {BcfViewpointPaths} from "core-app/modules/bim/bcf/api/viewpoints/bcf-viewpoint.paths";
export class BcfClickHandler extends CardClickHandler {
@InjectField() viewer:IFCViewerService;
@InjectField() states:States;
@InjectField() bcfApi:BcfApiService;
protected handleWorkPackage(wpId:string, element:JQuery<HTMLElement>, evt:JQuery.TriggeredEvent) {
this.setSelection(wpId, element, evt);
const wp = this.states.workPackages.get(wpId).value!;
// Open the viewpoint if any
if (this.viewer.viewerVisible() && wp.bcfViewpoints) {
this.viewer.showViewpoint(wp, 0);
}
}
}

@ -1,17 +0,0 @@
import {StateService} from "@uirouter/core";
import {CardDblClickHandler} from "core-components/wp-card-view/event-handler/double-click-handler";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {bimListViewIdentifier, BimViewService} from "core-app/modules/bim/ifc_models/pages/viewer/bim-view.service";
export class BcfDoubleClickHandler extends CardDblClickHandler {
@InjectField() state:StateService;
@InjectField() bimView:BimViewService;
protected handleWorkPackage(wpId:string) {
if (this.bimView.current === bimListViewIdentifier) {
this.state.go('work-packages.show', { workPackageId: wpId });
} else {
this.state.go('.details', { workPackageId: wpId });
}
}
}

@ -55,7 +55,8 @@
[cardsRemovable]="board.isFree && canDragOutOf"
[showInfoButton]="true"
[highlightingMode]="board.highlightingMode"
[showStatusButton]="showCardStatusButton()">
[showStatusButton]="showCardStatusButton()"
(itemClicked)="openFullViewOnDoubleClick($event)" >
</wp-card-view>
</div>

@ -465,4 +465,13 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
this.updateQuery(true);
});
}
openFullViewOnDoubleClick(event:{ workPackageId:string, double:boolean }) {
if (event.double) {
this.state.go(
'work-packages.show',
{ workPackageId: event.workPackageId }
);
}
}
}

@ -58,7 +58,7 @@ export class BackRoutingService {
} else {
if (this.keepTab.isDetailsState(this.backRoute.parent)) {
if (preferListOverSplit) {
this.$state.go(baseRoute, this.$state.params);
this.$state.go(baseRoute, this.backRoute.params);
} else {
this.$state.go(baseRoute + this.keepTab.currentDetailsSubState, this.backRoute.params);
}

@ -26,7 +26,7 @@
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {ChangeDetectorRef, Component, ElementRef, Inject} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, OnInit} from '@angular/core';
import {OpModalComponent} from 'core-components/op-modals/op-modal.component';
import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types';
import {HelpTextResource} from 'core-app/modules/hal/resources/help-text-resource';
@ -34,9 +34,10 @@ import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
@Component({
templateUrl: './help-text.modal.html'
templateUrl: './help-text.modal.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AttributeHelpTextModal extends OpModalComponent {
export class AttributeHelpTextModal extends OpModalComponent implements OnInit {
/* Close on escape? */
public closeOnEscape = true;
@ -58,6 +59,17 @@ export class AttributeHelpTextModal extends OpModalComponent {
super(locals, cdRef, elementRef);
}
ngOnInit() {
super.ngOnInit();
// Load the attachments
this
.helpText
.attachments
.$load()
.then(() => this.cdRef.detectChanges());
}
public get helpTextLink() {
if (this.helpText.editText) {
return this.helpText.editText.$link.href;

@ -21,6 +21,10 @@
[innerHtml]="helpText.helpText.html">
</div>
<attachments [resource]="helpText"
data-allow-uploading="false">
</attachments>
<div class="modal--form-actions">
<a class="help-text--edit-button button"
*ngIf="helpText.editText"

@ -31,8 +31,6 @@ import {Injector, NgModule} from "@angular/core";
import {AuthoringComponent} from 'core-app/modules/common/authoring/authoring.component';
import {OpDateTimeComponent} from 'core-app/modules/common/date/op-date-time.component';
import {AttributeHelpTextComponent} from 'core-app/modules/common/help-texts/attribute-help-text.component';
import {AttributeHelpTextModal} from 'core-app/modules/common/help-texts/attribute-help-text.modal';
import {OpIcon} from 'core-app/modules/common/icon/op-icon';
import {NotificationComponent} from 'core-app/modules/common/notifications/notification.component';
import {NotificationsContainerComponent} from 'core-app/modules/common/notifications/notifications-container.component';
@ -152,8 +150,6 @@ export function bootstrapModule(injector:Injector) {
OpIcon,
AutofocusDirective,
AttributeHelpTextComponent,
AttributeHelpTextModal,
FocusWithinDirective,
FocusDirective,
AuthoringComponent,
@ -164,9 +160,6 @@ export function bootstrapModule(injector:Injector) {
UploadProgressComponent,
OpDateTimeComponent,
// Entries for ng1 downgraded components
AttributeHelpTextComponent,
// Table highlight
HighlightColDirective,
@ -206,8 +199,6 @@ export function bootstrapModule(injector:Injector) {
OpIcon,
AutofocusDirective,
AttributeHelpTextComponent,
AttributeHelpTextModal,
FocusWithinDirective,
FocusDirective,
AuthoringComponent,
@ -221,9 +212,6 @@ export function bootstrapModule(injector:Injector) {
OPContextMenuComponent,
IconTriggeredContextMenuComponent,
// Entries for ng1 downgraded components
AttributeHelpTextComponent,
// Table highlight
HighlightColDirective,

@ -71,7 +71,7 @@ export class ContentTabsComponent extends ScrollableTabsComponent {
this.tabs = this.gonTabs.map((tab:GonTab) => {
return {
id: tab.name,
name: this.I18n.t('js.' + tab.label),
name: this.I18n.t('js.' + tab.label, { defaultValue: tab.label }),
path: tab.path
};
});

@ -3,6 +3,7 @@
(onRenamed)="renameWidget($event)">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -3,6 +3,7 @@
[editable]="isEditable">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -1,11 +1,13 @@
<h3 class="widget-box--header"
[ngClass]="{ '-editable': isRenameable }">
<ng-content select="[slot=prepend]"></ng-content>
<editable-toolbar-title [title]="name"
(onSave)="renamed($event)"
[editable]="isRenameable"
class="widget-box--header-title">
</editable-toolbar-title>
<ng-content></ng-content>
<ng-content select="[slot=menu]"></ng-content>
</h3>

@ -3,6 +3,7 @@
[editable]="isEditable">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -1,7 +1,9 @@
<widget-header [name]="widgetName"
[editable]="isEditable">
<widget-menu [resource]="resource">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -2,7 +2,12 @@
[name]="widgetName"
[editable]="isEditable">
<attribute-help-text slot="prepend"
attribute="description"
[attributeScope]="'Project'"></attribute-help-text>
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>
@ -11,7 +16,7 @@
<edit-form *ngIf="(project$ | async) as project"
[resource]="project">
<editable-attribute-field [resource]="project"
[fieldName]="'description'">
fieldName="description">
</editable-attribute-field>
</edit-form>
</div>

@ -3,6 +3,7 @@
[editable]="isEditable">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>
@ -14,6 +15,8 @@
<ng-container *ngFor="let cf of customFields">
<div class="attributes-map--key">
{{ cf.label }}
<attribute-help-text [attribute]="cf.key"
[attributeScope]="'Project'"></attribute-help-text>
</div>
<div class="attributes-map--value">
<editable-attribute-field [resource]="project"

@ -2,7 +2,12 @@
[name]="widgetName"
[editable]="isEditable">
<attribute-help-text slot="prepend"
attribute="status"
[attributeScope]="'Project'"></attribute-help-text>
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -3,6 +3,7 @@
[editable]="isEditable">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -3,6 +3,7 @@
[editable]="isEditable">
<widget-time-entries-current-user-menu
slot="menu"
[resource]="resource"
(onConfigured)="updateConfiguration($event)">
</widget-time-entries-current-user-menu>

@ -3,6 +3,7 @@
[editable]="isEditable">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -3,6 +3,7 @@
(onRenamed)="renameWidget($event)">
<widget-menu
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -3,6 +3,7 @@
(onRenamed)="renameWidget($event)">
<widget-wp-graph-menu
slot="menu"
[resource]="resource"
(onConfigured)="updateGraph($event)">
</widget-wp-graph-menu>

@ -3,7 +3,8 @@
(onRenamed)="renameWidget($event)">
<widget-menu
[resource]="resource">
slot="menu"
[resource]="resource">
</widget-menu>
</widget-header>

@ -2,6 +2,7 @@
[name]="widgetName"
(onRenamed)="renameWidget($event)">
<widget-wp-table-menu
slot="menu"
[resource]="resource">
</widget-wp-table-menu>
</widget-header>

@ -28,8 +28,9 @@
import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
import {CallableHalLink} from 'core-app/modules/hal/hal-link/hal-link';
import {Attachable} from "core-app/modules/hal/resources/mixins/attachable-mixin";
export class HelpTextResource extends HalResource {
export class HelpTextBaseResource extends HalResource {
public id:string;
public attribute:string;
@ -38,6 +39,8 @@ export class HelpTextResource extends HalResource {
public helpText:api.v3.Formattable;
}
export interface HelpTextResource {
export const HelpTextResource = Attachable(HelpTextBaseResource);
export interface HelpTextResource extends HelpTextBaseResource {
editText?:CallableHalLink;
}

@ -37,7 +37,6 @@ import {
import {HookService} from 'core-app/modules/plugins/hook-service';
import {WorkPackageEmbeddedTableComponent} from 'core-components/wp-table/embedded/wp-embedded-table.component';
import {WorkPackageEmbeddedTableEntryComponent} from 'core-components/wp-table/embedded/wp-embedded-table-entry.component';
import {WorkPackagesTableController} from 'core-components/wp-table/wp-table.directive';
import {WorkPackageTablePaginationComponent} from 'core-components/wp-table/table-pagination/wp-table-pagination.component';
import {WpResizerDirective} from 'core-components/resizer/wp-resizer.component';
import {WorkPackageTimelineTableController} from 'core-components/wp-table/timeline/container/wp-timeline-container.directive';
@ -130,7 +129,6 @@ import {WorkPackageCacheService} from 'core-components/work-packages/work-packag
import {SchemaCacheService} from 'core-components/schemas/schema-cache.service';
import {WorkPackageWatchersService} from 'core-components/wp-single-view-tabs/watchers-tab/wp-watchers.service';
import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service';
import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service';
import {QueryFormDmService} from 'core-app/modules/hal/dm-services/query-form-dm.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageChildrenQueryComponent} from "core-components/wp-relations/embedded/children/wp-children-query.component";
@ -169,6 +167,9 @@ import {WorkPackageViewPageComponent} from "core-app/modules/work_packages/routi
import {WorkPackageSettingsButtonComponent} from "core-components/wp-buttons/wp-settings-button/wp-settings-button.component";
import {BackButtonComponent} from "core-app/modules/common/back-routing/back-button.component";
import {DatePickerModal} from "core-components/datepicker/datepicker.modal";
import {WorkPackagesTableComponent} from "core-components/wp-table/wp-table.component";
import {AttributeHelpTextComponent} from "core-app/modules/common/help-texts/attribute-help-text.component";
import {AttributeHelpTextModal} from "core-app/modules/common/help-texts/attribute-help-text.modal";
@NgModule({
imports: [
@ -257,7 +258,7 @@ import {DatePickerModal} from "core-components/datepicker/datepicker.modal";
WorkPackagesGridComponent,
WorkPackagesTableController,
WorkPackagesTableComponent,
WorkPackagesTableConfigMenu,
WorkPackageTablePaginationComponent,
@ -379,9 +380,13 @@ import {DatePickerModal} from "core-components/datepicker/datepicker.modal";
WorkPackageCardViewComponent,
WorkPackageSingleCardComponent,
WorkPackageViewToggleButton,
// Help texts
AttributeHelpTextComponent,
AttributeHelpTextModal,
],
exports: [
WorkPackagesTableController,
WorkPackagesTableComponent,
WorkPackageTablePaginationComponent,
WorkPackageEmbeddedTableComponent,
WorkPackageEmbeddedTableEntryComponent,
@ -412,7 +417,11 @@ import {DatePickerModal} from "core-components/datepicker/datepicker.modal";
WorkPackageEditActionsBarComponent,
WorkPackageSingleViewComponent,
WorkPackageSplitViewComponent,
BackButtonComponent
BackButtonComponent,
// Help texts
AttributeHelpTextComponent,
AttributeHelpTextModal,
]
})
export class OpenprojectWorkPackagesModule {

@ -5,6 +5,8 @@
<wp-table *ngIf="tableInformationLoaded && showTableView"
[projectIdentifier]="CurrentProject.identifier"
[configuration]="wpTableConfiguration"
(itemClicked)="handleWorkPackageClicked($event)"
(stateLinkClicked)="openStateLink($event)"
class="work-packages-split-view--tabletimeline-content">
</wp-table>
@ -14,6 +16,8 @@
[ngClass]="{ '-with-resizer': showResizerInCardView() }" >
<wp-grid [configuration]="wpTableConfiguration"
[showResizer]="showResizerInCardView()"
(itemClicked)="handleWorkPackageCardClicked($event)"
(stateLinkClicked)="openStateLink($event)"
resizerClass="work-packages-partitioned-page--content-right"
resizerStorageKey="openProject-splitViewFlexBasis">
</wp-grid>

@ -44,6 +44,8 @@ import {CurrentProjectService} from "core-components/projects/current-project.se
import {WorkPackageViewFiltersService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-filters.service";
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin";
import {QueryResource} from "core-app/modules/hal/resources/query-resource";
import {StateService} from "@uirouter/core";
import {KeepTabService} from "core-components/wp-single-view-tabs/keep-tab/keep-tab.service";
@Component({
selector: 'wp-list-view',
@ -84,6 +86,8 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements
constructor(readonly I18n:I18nService,
readonly injector:Injector,
readonly $state:StateService,
readonly keepTab:KeepTabService,
readonly querySpace:IsolatedQuerySpace,
readonly wpViewFilters:WorkPackageViewFiltersService,
readonly deviceService:DeviceService,
@ -127,4 +131,36 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements
this.showTableView = !(this.deviceService.isMobile ||
this.wpDisplayRepresentation.valueFromQuery(query) === wpDisplayCardRepresentation);
}
handleWorkPackageClicked(event:{ workPackageId:string; double:boolean }) {
if (event.double) {
this.openInFullView(event.workPackageId);
}
}
openStateLink(event:{ workPackageId:string; requestedState:string }) {
this.$state.go(
(this.keepTab as any)[event.requestedState] || event.requestedState,
{ workPackageId: event.workPackageId, focus: true }
);
}
/**
* Special handling for clicking on cards.
* If we are on mobile, a click on the card should directly open the full view
*/
handleWorkPackageCardClicked(event:{ workPackageId:string; double:boolean }) {
if (this.deviceService.isMobile) {
this.openInFullView(event.workPackageId);
} else {
this.handleWorkPackageClicked(event);
}
}
private openInFullView(workPackageId:string) {
this.$state.go(
'work-packages.show',
{ workPackageId: workPackageId }
);
}
}

@ -1,4 +1,4 @@
import {InjectionToken, Injector} from '@angular/core';
import {EventEmitter, InjectionToken, Injector} from '@angular/core';
export interface WorkPackageViewEventHandler<T> {
/** Event name to register **/
@ -14,8 +14,13 @@ export interface WorkPackageViewEventHandler<T> {
eventScope(view:T):JQuery;
}
export interface WorkPackageViewHandlerClass<T> {
new(injector:Injector):WorkPackageViewEventHandler<any>;
export interface WorkPackageViewOutputs {
// On selection updated
selectionChanged:EventEmitter<string[]>;
// On row (double) clicked
itemClicked:EventEmitter<{ workPackageId:string, double:boolean }>;
// On work package link / details icon clicked
stateLinkClicked:EventEmitter<{ workPackageId:string, requestedState:string }>;
}
export const WorkPackageViewHandlerToken = new InjectionToken<WorkPackageViewEventHandler<any>>('CardEventHandler');

@ -242,6 +242,15 @@ module API
form_embedded: form_embedded)
end
def self.representable_definitions
representable_config = self.representable_attrs
# For reasons beyond me, Representable::Config contains the definitions
# * nested in [:definitions] in some envs, e.g. development
# * directly in other envs, e.g. test
representable_config.try(:definitions) || representable_config
end
def initialize(represented,
self_link = nil,
current_user:,

@ -0,0 +1,52 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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 API
module V3
module Attachments
class AttachmentsByHelpTextAPI < ::API::OpenProjectAPI
resources :attachments do
helpers API::V3::Attachments::AttachmentsByContainerAPI::Helpers
helpers do
def container
@help_text
end
def get_attachment_self_path
api_v3_paths.attachments_by_help_text(container.id)
end
end
get &API::V3::Attachments::AttachmentsByContainerAPI.read
post &API::V3::Attachments::AttachmentsByContainerAPI.create
end
end
end
end
end

@ -32,12 +32,16 @@ module API
module V3
module HelpTexts
class HelpTextRepresenter < ::API::Decorators::Single
include API::Decorators::LinkedResource
include API::Caching::CachedRepresenter
include ::API::V3::Attachments::AttachableRepresenterMixin
self_link path: :help_text,
id_attribute: :id,
title_getter: ->(*) { nil }
link :editText do
if current_user.admin?
if current_user.admin? && represented.persisted?
{
href: edit_attribute_help_text_path(represented.id),
type: 'text/html'

@ -47,6 +47,8 @@ module API
get do
HelpTextRepresenter.new(@help_text, current_user: current_user)
end
mount ::API::V3::Attachments::AttachmentsByHelpTextAPI
end
end
end

@ -115,6 +115,10 @@ module API
"#{work_package(id)}/attachments"
end
def self.attachments_by_help_text(id)
"#{help_text(id)}/attachments"
end
def self.attachments_by_wiki_page(id)
"#{wiki_page(id)}/attachments"
end

@ -66,6 +66,7 @@ module Redmine
add_on_new_permission: add_on_new_permission(options),
add_on_persisted_permission: add_on_persisted_permission(options),
only_user_allowed: only_user_allowed(options),
viewable_by_all_users: viewable_by_all_users(options),
modification_blocked: options[:modification_blocked],
extract_tsv: attachable_extract_tsv_option(options)
}
@ -80,6 +81,7 @@ module Redmine
:add_on_persisted_permission,
:add_permission,
:only_user_allowed,
:viewable_by_all_users,
:modification_blocked,
:extract_tsv)
end
@ -100,6 +102,10 @@ module Redmine
options[:add_on_persisted_permission] || options[:add_permission] || edit_permission_default
end
def viewable_by_all_users(options)
options.fetch(:viewable_by_all_users, false)
end
def only_user_allowed(options)
options.fetch(:only_user_allowed, false)
end
@ -149,6 +155,8 @@ module Redmine
end
def attachments_visible?(user = User.current)
return true if user.logged? && self.class.attachable_options[:viewable_by_all_users]
allowed_to_on_attachment?(user, self.class.attachable_options[:view_permission])
end

@ -36,7 +36,6 @@ module Bim
::Bim::BasicData::ActivitySeeder,
::BasicData::ColorSeeder,
::BasicData::ColorSchemeSeeder,
::Bim::BasicData::CustomStyleSeeder,
::Bim::BasicData::WorkflowSeeder,
::Bim::BasicData::PrioritySeeder,
::Bim::BasicData::SettingSeeder,

@ -40,12 +40,13 @@ describe 'Work Package boards spec', type: :feature, js: true do
let(:project) { FactoryBot.create(:project, identifier: 'boards', enabled_module_names: %i[work_package_tracking board_view]) }
let(:permissions) { %i[show_board_views manage_board_views add_work_packages view_work_packages manage_public_queries] }
let(:role) { FactoryBot.create(:role, permissions: permissions) }
let(:admin) { FactoryBot.create :admin }
let!(:priority) { FactoryBot.create :default_priority }
let!(:status) { FactoryBot.create :default_status }
let(:board_index) { Pages::BoardIndex.new(project) }
let!(:board_view) { FactoryBot.create :board_grid_with_query, name: 'My board', project: project }
let(:project_html_title) { ::Components::HtmlTitle.new project }
let(:destroy_modal) { Components::WorkPackages::DestroyModal.new }
before do
with_enterprise_token :board_view
@ -140,4 +141,39 @@ describe 'Work Package boards spec', type: :feature, js: true do
expect(page).to have_current_path /details\/#{wp.id}\/relations/
split_view.expect_tab 'Relations'
end
before do
with_enterprise_token :board_view
project
login_as(admin)
end
it 'navigates to boards after deleting WP(see #33756)' do
board_index.visit!
# Add a new WP on the board
board_page = board_index.open_board board_view
board_page.expect_query 'List 1', editable: true
board_page.add_card 'List 1', 'Task 1'
board_page.expect_notification message: I18n.t(:notice_successful_create)
wp = WorkPackage.last
expect(wp.subject).to eq 'Task 1'
# Open the details page with the info icon
card = board_page.card_for(wp)
split_view = card.open_details_view
split_view.expect_subject
# Go to full view of WP
split_view.switch_to_fullscreen
find('#action-show-more-dropdown-menu').click
click_link(I18n.t('js.button_delete'))
# Delete the WP
destroy_modal.expect_listed(wp)
destroy_modal.confirm_deletion
board_page.expect_empty
board_page.expect_path
end
end

@ -53,6 +53,10 @@ module Pages
!(free? || action_attribute.nil?)
end
def expect_path
expect(page).to have_current_path /boards\/#{@board.id}/
end
def action_attribute
@board.options['attribute']
end
@ -213,7 +217,7 @@ module Pages
end
def expect_empty
expect(page).to have_no_selector('.boards-list--item')
expect(page).to have_no_selector('.boards-list--item', wait: 10)
end
def remove_list(name)

@ -666,7 +666,7 @@ describe CostlogController, type: :controller do
before do
grant_current_user_permissions user, [:edit_cost_entries]
params['cost_entry']['cost_type_id'] = '1'
params['cost_entry']['cost_type_id'] = '1234123512'
end
it_should_behave_like 'invalid update'

@ -0,0 +1,53 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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.
#++
require 'spec_helper'
describe AttributeHelpTexts::BaseContract do
let(:model) { FactoryBot.build_stubbed :work_package_help_text }
let(:contract) { described_class.new(model, current_user) }
subject { contract.validate }
context 'as admin' do
let(:current_user) { FactoryBot.build_stubbed :admin }
it 'validates the contract' do
expect(subject).to eq true
end
end
context 'as regular user' do
let(:current_user) { FactoryBot.build_stubbed :user }
it 'returns an error on validation' do
expect(subject).to eq false
expect(contract.errors.symbols_for(:base))
.to match_array [:error_unauthorized]
end
end
end

@ -1,6 +1,7 @@
require 'spec_helper'
describe AttributeHelpTextsController, type: :controller do
let(:user) { FactoryBot.build_stubbed :admin }
let(:model) { FactoryBot.build :work_package_help_text }
let(:relation_columns_allowed) { true }
@ -13,8 +14,7 @@ describe AttributeHelpTextsController, type: :controller do
before do
with_enterprise_token(relation_columns_allowed ? :attribute_help_texts : nil)
expect(controller).to receive(:require_admin)
login_as user
end
describe '#index' do

@ -4,4 +4,10 @@ FactoryBot.define do
help_text { 'Attribute help text' }
attribute_name { 'status' }
end
factory :project_help_text, class: AttributeHelpText::Project do
type { 'AttributeHelpText::Project' }
help_text { 'Attribute help text' }
attribute_name { 'status' }
end
end

@ -34,6 +34,7 @@ describe 'Attribute help texts' do
let(:instance) { AttributeHelpText.last }
let(:modal) { Components::AttributeHelpTextModal.new(instance) }
let(:editor) { Components::WysiwygEditor.new }
let(:image_fixture) { Rails.root.join('spec/fixtures/files/image.png') }
let(:relation_columns_allowed) { true }
@ -57,13 +58,31 @@ describe 'Attribute help texts' do
# -> create
select 'Status', from: 'attribute_help_text_attribute_name'
editor.set_markdown('My attribute help text')
# Add an image
# adding an image
editor.drag_attachment image_fixture, 'Image uploaded on creation'
expect(page).to have_selector('attachment-list-item', text: 'image.png')
click_button 'Save'
# Should now show on index for editing
expect(page).to have_selector('.attribute-help-text--entry td', text: 'Status')
expect(instance.attribute_scope).to eq 'WorkPackage'
expect(instance.attribute_name).to eq 'status'
expect(instance.help_text).to eq 'My attribute help text'
expect(instance.help_text).to include 'My attribute help text'
expect(instance.help_text).to match /\/api\/v3\/attachments\/\d+\/content/
# Open help text modal
modal.open!
expect(modal.modal_container).to have_text 'My attribute help text'
expect(modal.modal_container).to have_selector('img')
modal.expect_edit(admin: true)
# Expect files section to be present
expect(modal.modal_container).to have_selector('.form--fieldset-legend', text: 'FILES')
expect(modal.modal_container).to have_selector('.work-package--attachments--filename')
modal.close!
# -> edit
page.find('.attribute-help-text--entry td a', text: 'Status').click

@ -0,0 +1,96 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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.
#++
require 'spec_helper'
describe 'Project attribute help texts', type: :feature, js: true do
let(:project) { FactoryBot.create :project }
let(:instance) do
FactoryBot.create :project_help_text,
attribute_name: :status,
help_text: 'Some **help text** for status.'
end
let(:grid) do
grid = FactoryBot.create :grid
grid.widgets << FactoryBot.create(:grid_widget,
identifier: 'project_status',
options: { 'name' => 'Project status' },
start_row: 1,
end_row: 2,
start_column: 1,
end_column: 1)
end
let(:modal) { Components::AttributeHelpTextModal.new(instance) }
let(:wp_page) { Pages::FullWorkPackage.new work_package }
before do
login_as user
project
instance
end
shared_examples 'allows to view help texts' do
it 'shows an indicator for whatever help text exists' do
visit project_path(project)
within '#menu-sidebar' do
click_link "Overview"
end
expect(page).to have_selector('.widget-box--header .help-text--entry', wait: 10)
# Open help text modal
modal.open!
expect(modal.modal_container).to have_selector('strong', text: 'help text')
modal.expect_edit(admin: user.admin?)
modal.close!
end
end
describe 'as admin' do
let(:user) { FactoryBot.create :admin }
it_behaves_like 'allows to view help texts'
end
describe 'as regular user' do
let(:view_role) do
FactoryBot.create :role, permissions: [:view_project]
end
let(:user) do
FactoryBot.create :user,
member_in_project: project,
member_through_role: view_role
end
it_behaves_like 'allows to view help texts'
end
end

@ -51,6 +51,13 @@ describe ::API::V3::HelpTexts::HelpTextRepresenter do
"editText" => {
"href" => edit_attribute_help_text_path(help_text.id),
"type" => "text/html"
},
"attachments" => {
"href" => api_v3_paths.attachments_by_help_text(help_text.id)
},
"addAttachment" => {
"href" => api_v3_paths.attachments_by_help_text(help_text.id),
"method" => "post"
}
},
"id" => help_text.id,

@ -29,6 +29,11 @@
require 'spec_helper'
describe AttributeHelpText::WorkPackage, type: :model do
it_behaves_like 'acts_as_attachable included' do
let(:model_instance) { FactoryBot.create(:work_package_help_text) }
let(:project) { FactoryBot.create(:project) }
end
describe '.available_attributes' do
subject { described_class.available_attributes }
it 'returns an array of potential attributes' do

@ -153,4 +153,18 @@ shared_examples_for 'acts_as_attachable included' do
end
end
end
describe '#attachments_visible' do
let!(:attachment1) { FactoryBot.create(:attachment, container: model_instance, author: current_user) }
it 'allows access to a logged user when viewable_by_all_users is set' do
if model_instance.class.attachable_options[:viewable_by_all_users]
expect(model_instance.attachments_visible?(other_user)).to eq true
expect(attachment1.visible?(no_permission_user)).to eq true
else
expect(model_instance.attachments_visible?(other_user)).to eq false
expect(attachment1.visible?(other_user)).to eq false
end
end
end
end

Loading…
Cancel
Save