Merge remote-tracking branch 'origin/wp-edit-sync' into wp-edit-sync

Conflicts:
	frontend/app/components/wp-panels/overview-panel/overview-panel.directive.ts
pull/4338/head
Roman Roelofsen 9 years ago
commit fe8007fa19
  1. 2
      Gemfile.lock
  2. 6
      app/assets/stylesheets/content/_context_menu.sass
  3. 6
      app/assets/stylesheets/content/_forms.lsg
  4. 4
      app/assets/stylesheets/content/_forms.sass
  5. 2
      app/assets/stylesheets/content/_work_packages_table_edit.sass
  6. 1
      app/assets/stylesheets/default.css.sass
  7. 1
      app/assets/stylesheets/layout/_toolbar.sass
  8. 6
      app/assets/stylesheets/specific/announcements.sass
  9. 30
      app/controllers/announcements_controller.rb
  10. 1
      app/controllers/homescreen_controller.rb
  11. 9
      app/helpers/announcements_helper.rb
  12. 11
      app/helpers/types_helper.rb
  13. 26
      app/models/announcement.rb
  14. 2
      app/views/account/exit.html.erb
  15. 2
      app/views/account/login.html.erb
  16. 10
      app/views/announcements/_show.html.erb
  17. 22
      app/views/announcements/edit.html.erb
  18. 2
      app/views/boards/_form.html.erb
  19. 2
      app/views/boards/show.html.erb
  20. 2
      app/views/categories/_form.html.erb
  21. 2
      app/views/homescreen/index.html.erb
  22. 2
      app/views/messages/edit.html.erb
  23. 2
      app/views/messages/new.html.erb
  24. 4
      app/views/messages/show.html.erb
  25. 2
      app/views/news/edit.html.erb
  26. 2
      app/views/news/new.html.erb
  27. 2
      app/views/project_associations/new.html.erb
  28. 2
      app/views/reportings/edit.html.erb
  29. 2
      app/views/reportings/new.html.erb
  30. 2
      app/views/repositories/committers.html.erb
  31. 2
      app/views/timelines/filter/_projects.html.erb
  32. 66
      app/views/types/_form.html.erb
  33. 2
      app/views/versions/edit.html.erb
  34. 2
      app/views/versions/new.html.erb
  35. 2
      app/views/wiki/edit.html.erb
  36. 2
      app/views/wiki/new.html.erb
  37. 9
      codecov.yml
  38. 9
      config/initializers/menus.rb
  39. 14
      config/locales/en.yml
  40. 7
      config/locales/js-en.yml
  41. 3
      config/routes.rb
  42. 28
      db/migrate/20121114100641_aggregated_announcements_migrations.rb
  43. 2
      doc/operation_guides/manual/installation-guide.md
  44. 53
      frontend/app/angular-modules.ts
  45. 71
      frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts
  46. 5
      frontend/app/components/common/notification-box/notification-box.directive.html
  47. 23
      frontend/app/components/common/notification-box/notification-box.directive.js
  48. 0
      frontend/app/components/common/notification-box/notification-box.directive.test.js
  49. 132
      frontend/app/components/common/notifications/notifications.service.js
  50. 84
      frontend/app/components/common/notifications/notifications.service.test.js
  51. 2
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.service.html
  52. 13
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.service.test.ts
  53. 18
      frontend/app/components/inplace-edit/wp-field/wp-field.service.js
  54. 14
      frontend/app/components/routing/ui-router.config.ts
  55. 1
      frontend/app/components/routing/wp-list/wp.list.html
  56. 115
      frontend/app/components/routing/wp-show/wp-show.controller.js
  57. 56
      frontend/app/components/routing/wp-show/wp.show.html
  58. 16
      frontend/app/components/work-packages/wp-attachments/wp-attachments.service.test.ts
  59. 109
      frontend/app/components/work-packages/wp-attachments/wp-attachments.service.ts
  60. 18
      frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.html
  61. 2
      frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.test.ts
  62. 20
      frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts
  63. 13
      frontend/app/components/work-packages/wp-new.controller.js
  64. 299
      frontend/app/components/work-packages/wp-single-view/wp-single-view-field.service.ts
  65. 63
      frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.html
  66. 125
      frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.ts
  67. 140
      frontend/app/components/work-packages/wp-single-view/wp-single-view.service.ts
  68. 15
      frontend/app/components/wp-buttons/wp-inline-create-button/wp-inline-create-button.controller.ts
  69. 6
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts
  70. 18
      frontend/app/components/wp-edit/wp-edit-form.directive.ts
  71. 11
      frontend/app/components/wp-panels/overview-panel/overview-panel.directive.html
  72. 39
      frontend/app/components/wp-panels/overview-panel/overview-panel.directive.ts
  73. 17
      frontend/app/components/wp-panels/overview-panel/wp-overview-panel.directive.html
  74. 48
      frontend/app/components/wp-table/context-menu-helper/wp-context-menu-helper.service.js
  75. 35
      frontend/app/components/wp-table/wp-group-header/wp-group-header.directive.js
  76. 33
      frontend/app/components/wp-table/wp-table.directive.html
  77. 44
      frontend/app/components/wp-table/wp-table.directive.js
  78. 7
      frontend/app/components/wp-table/wp-table.service.js
  79. 5
      frontend/app/services/index.js
  80. 130
      frontend/app/services/notifications-service.js
  81. 73
      frontend/app/templates/work_packages/tabs/overview.html
  82. 8
      frontend/app/ui_components/index.js
  83. 104
      frontend/app/work_packages/controllers/details-tab-overview-controller.js
  84. 11
      frontend/app/work_packages/controllers/index.js
  85. 2
      frontend/app/work_packages/directives/work-package-attachments-directive.js
  86. 4
      frontend/app/work_packages/helpers/index.js
  87. 14
      frontend/app/work_packages/helpers/work-package-display-helper.js
  88. 10
      frontend/app/work_packages/services/index.js
  89. 105
      frontend/app/work_packages/services/work-package-attachments-service.js
  90. 22
      frontend/tests/unit/tests/work_packages/helpers/work-package-context-menu-helper-test.js
  91. 2
      frontend/webpack.config.js
  92. 2
      lib/api/decorators/aggregation_group.rb
  93. 2
      lib/api/decorators/allowed_values_by_collection_representer.rb
  94. 5
      lib/api/decorators/property_schema_representer.rb
  95. 70
      lib/api/decorators/schema.rb
  96. 2
      lib/api/v3/projects/project_collection_representer.rb
  97. 2
      lib/api/v3/projects/project_representer.rb
  98. 1
      lib/api/v3/utilities/custom_field_injector.rb
  99. 4
      lib/api/v3/work_packages/available_projects_on_create_api.rb
  100. 4
      lib/api/v3/work_packages/available_projects_on_edit_api.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -53,7 +53,7 @@ GIT
GIT
remote: https://github.com/opf/openproject-translations.git
revision: 87a7df42600c7edc084007c2c147361ea94933c4
revision: af89ef0a50fe76c243fdee253f1b1fa439488a46
branch: dev
specs:
openproject-translations (5.1.0)

@ -26,12 +26,6 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
@mixin context-menu-defaults
display:block
margin: 0
padding: 0
border: 0
#work-package-context-menu, #column-context-menu
&.action-menu
position: absolute

@ -13,7 +13,7 @@
</div>
</div>
<hr class="form--separator">
<hr class="form--separator" />
<button class="button -highlight">Save</button>
<button class="button">Cancel</button>
</form>
@ -32,7 +32,7 @@
</div>
</div>
<hr class="form--separator">
<hr class="form--separator" />
<button class="button -highlight">Save</button>
<button class="button">Cancel</button>
</form>
@ -51,7 +51,7 @@
</div>
</div>
<hr class="form--separator">
<hr class="form--separator" />
<button class="button -highlight">Save</button>
<button class="button">Cancel</button>
</form>

@ -178,8 +178,6 @@ $form--field-types: (text-field, text-area, select, check-box, radio-button, ran
\:checked + .styled-checkbox:after
opacity: 1
\:focus + .styled-checkbox:before
.styled-checkbox
box-sizing: content-box
display: inline-block
@ -436,7 +434,7 @@ fieldset.form--fieldset
&.add_locale.icon
margin-top: 5px
&:before
padding-left: 0px
padding-left: 0px
%form--field-element-container
display: block

@ -66,7 +66,6 @@
// Editable fields cursor
.-editable .wp-table--cell-span
padding: 0 5px 0 5px
cursor: text
border-color: transparent
border-style: solid
@ -85,6 +84,7 @@
// Default cursor on non-editable and id fields
.wp-table--cell-span
padding: 0 5px 0 5px
cursor: not-allowed
&.id

@ -112,6 +112,7 @@
@import content/select2
@import specific/homescreen
@import specific/announcements
@import misc_legacy
@import jstoolbar

@ -67,7 +67,6 @@
display: table
.toolbar-items
> li
.toolbar-item,
.toolbar-button-group > li
float: left

@ -0,0 +1,6 @@
#announcement
margin-left: auto
margin-right: auto
margin-top: 1rem
width: 511px

@ -0,0 +1,30 @@
class AnnouncementsController < ApplicationController
layout 'admin'
before_filter :require_admin
def edit
@announcement = Announcement.only_one
end
def update
@announcement = Announcement.only_one
@announcement.attributes = announcement_params
if @announcement.save
flash[:success] = t(:notice_successful_update)
end
render action: 'edit'
end
private
def default_breadcrumb
t(:label_announcement)
end
def announcement_params
params.require(:announcement).permit('text', 'show_until', 'active')
end
end

@ -32,6 +32,7 @@ class HomescreenController < ApplicationController
@newest_projects = Project.visible.newest.take(3)
@newest_users = User.newest.take(3)
@news = News.latest(count: 3)
@announcement = Announcement.active_and_current
@homescreen = OpenProject::Homescreen
end

@ -0,0 +1,9 @@
module AnnouncementsHelper
def notice_annoucement_active
if @announcement.active_and_current?
l(:'announcements.is_active')
else
l(:'announcements.is_inactive')
end
end
end

@ -69,7 +69,7 @@ module ::TypesHelper
# within the form date is shown as a single entry including start and due
if merge_date
attributes['date'] = { required: false }
attributes['date'] = { required: false, has_default: false }
attributes.delete 'due_date'
attributes.delete 'start_date'
end
@ -77,6 +77,7 @@ module ::TypesHelper
WorkPackageCustomField.all.each do |field|
attributes["custom_field_#{field.id}"] = {
required: field.is_required,
has_default: field.default_value,
display_name: field.name
}
end
@ -102,6 +103,14 @@ module ::TypesHelper
end
end
def translated_attribute_name(name, attr)
if attr[:name_source]
attr[:name_source].call
else
attr[:display_name] || attr_translate(name)
end
end
##
# There isn't actually a 'date' field for work packages.
# There are two fields: 'start_date' and 'due_date'

@ -0,0 +1,26 @@
class Announcement < ActiveRecord::Base
scope :active, -> { where(active: true) }
scope :current, -> { where('show_until >= ?', Date.today) }
validates :show_until, presence: true
def self.active_and_current
active.current.first
end
def self.only_one
a = first
a = create_default_announcement if a.nil?
a
end
def active_and_current?
active? && show_until && show_until >= Date.today
end
def self.create_default_announcement
Announcement.create text: 'Announcement',
show_until: Date.today + 14.days,
active: false
end
end

@ -44,7 +44,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div id="login-form" class="form -bordered">
<h1><%= I18n.t(:label_login) %></h1>
<hr class="form--separator">
<hr class="form--separator" />
<p><%= instruction_text.html_safe %></p>
</div>
<%= call_hook :view_account_login_bottom %>

@ -35,7 +35,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div id="login-form" class="form -bordered">
<h1><%= I18n.t(:label_login) %></h1>
<hr class="form--separator">
<hr class="form--separator" />
<% unless OpenProject::Configuration.disable_password_login? %>
<%= render partial: 'password_login_form' %>
<% end %>

@ -0,0 +1,10 @@
<% announcement = Announcement.active_and_current %>
<% if announcement.present? %>
<div id="announcement">
<div class="notification-box -info">
<div class="notification-box--content">
<%= format_text announcement.text %>
</div>
</div>
</div>
<% end %>

@ -0,0 +1,22 @@
<% html_title t(:label_administration), t(:label_announcement) %>
<%= error_messages_for 'announcement' %>
<%= toolbar title: "#{t(:label_announcement)} (#{notice_annoucement_active})" %>
<%= labelled_tabular_form_for @announcement,
:url => {:action => :update},
:html => {:method => :put} do |f|%>
<div class="form--field">
<%= f.text_area :text, :cols => 80, :rows => 5, label: t(:label_text) %>
</div>
<div class="form--field">
<%= f.text_field :show_until, label: t('announcements.show_until') %>
<%= calendar_for("announcement_show_until") %>
</div>
<div class="form--field">
<%= f.check_box :active, label: t(:label_active) %>
</div>
<hr class="form-separator">
<%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %>
<% end %>

@ -34,5 +34,5 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field">
<%= f.text_field :description, required: true %>
</div>
<hr class="form--separator">
<hr class="form--separator" />
<!--[eoform:board]-->

@ -39,7 +39,7 @@ See doc/COPYRIGHT.rdoc for more details.
class: 'form' } do |f| %>
<%= render partial: 'messages/form', locals: {f: f} %>
<hr class="form--separator">
<hr class="form--separator" />
<%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-checkmark' %>
<%= preview_link preview_board_topics_path(@board), 'message-form-preview' %>
<%= link_to l(:button_cancel), '', onclick: 'Element.hide("add-message")', class: 'button' %>

@ -38,4 +38,4 @@ See doc/COPYRIGHT.rdoc for more details.
include_blank: true %>
</div>
<hr class="form--separator">
<hr class="form--separator" />

@ -34,6 +34,8 @@ See doc/COPYRIGHT.rdoc for more details.
</h2>
</div>
<%= render partial: 'announcements/show' %>
<% if @homescreen[:blocks].any? %>
<section class="widget-boxes -flex">
<% @homescreen[:blocks].each do |block| %>

@ -37,7 +37,7 @@ See doc/COPYRIGHT.rdoc for more details.
class: 'form' } do |f| %>
<%= render partial: 'form', locals: { f: f, replying: !@message.parent.nil? } %>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l(:button_save), class: 'button -highlight -with-icon icon-checkmark' %>
<%= preview_link preview_topic_path(@message), 'message-form-preview' %>

@ -36,7 +36,7 @@ See doc/COPYRIGHT.rdoc for more details.
class: 'form' } do |f| %>
<%= render partial: 'messages/form', locals: { f: f } %>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l(:button_create), class: 'button -highlight -with-icon icon-checkmark' %>
<%= preview_link preview_board_topics_path(@board), 'message-form-preview' %>

@ -123,7 +123,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div id="reply" style="display:none;">
<hr class="form--separator">
<hr class="form--separator" />
<%= labelled_tabular_form_for @reply,
as: :reply,
@ -133,7 +133,7 @@ See doc/COPYRIGHT.rdoc for more details.
class: 'form' } do |f| %>
<%= render partial: 'form', locals: {f: f, replying: true} %>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l(:button_submit), class: 'button -highlight -highlight -with-icon icon-checkmark' %>
<%= preview_link preview_topic_path(@message), 'message-form-preview' %>

@ -32,7 +32,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= labelled_tabular_form_for @news, html: { id: 'news-form' } do |f| %>
<%= render partial: 'form', locals: { f: f } %>
<hr class="form--separator">
<hr class="form--separator" />
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
<%= preview_link preview_news_path(@news), 'news-form-preview' %>
<% end %>

@ -33,7 +33,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= labelled_tabular_form_for [@project, @news], html: { id: 'news-form' } do |f| %>
<%= render partial: 'news/form', locals: { f: f } %>
<hr class="form--separator">
<hr class="form--separator" />
<%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-checkmark' %>
<%= preview_link preview_news_index_path, 'news-form-preview' %>
<% end %>

@ -72,7 +72,7 @@ See doc/COPYRIGHT.rdoc for more details.
</span>
<%= render partial: 'form', locals: {f: f, project: @project} %>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l("timelines.add_project_association"), name: nil,
class: 'button -highlight -with-icon icon-checkmark'%>
<%= link_to l(:button_cancel), project_project_associations_path(@project),

@ -62,7 +62,7 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
<%= wikitoolbar_for 'reporting_reported_project_status_comment' %>
</fieldset>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l(:button_save), name: nil, class: 'button -highlight -with-icon icon-checkmark' %>
<%= link_to l(:button_cancel), project_reportings_path(@project), class: 'button' %>
<% end %>

@ -72,7 +72,7 @@ See doc/COPYRIGHT.rdoc for more details.
</span>
</fieldset>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l(:button_create), name: nil, class: 'button -highlight -with-icon icon-checkmark' %>
<%= link_to l(:button_cancel), project_reportings_path(@project), class: 'button' %>
<% end %>

@ -78,7 +78,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="generic-table--header-background"></div>
</div>
</div>
<hr class="form--separator">
<hr class="form--separator" />
<p>
<%= submit_tag l(:button_update), class: 'button -highlight' %>
<%= link_to settings_project_path(id: @project.id, tab: 'repository'), class: 'button' do %>

@ -178,7 +178,7 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
<div class="form--grouping-row">
<hr class="form--separator">
<hr class="form--separator" />
</div>
<div class="form--grouping-row">

@ -50,37 +50,21 @@ See doc/COPYRIGHT.rdoc for more details.
<!--[eoform:type]-->
</section>
</div>
<div class="grid-content medium-6">
<% if @projects.any? %>
<fieldset class="form--fieldset" id="type_project_ids">
<legend class="form--fieldset-legend"><%= l(:label_project_plural) %></legend>
<div class="form--fieldset-control">
<span class="form--fieldset-control-container">
(<%= check_all_links 'type_project_ids' %>)
</span>
</div>
<%= project_nested_ul(@projects) do |p|
content_tag('label',
check_box_tag('type[project_ids][]', p.id, controller.type.projects.include?(p), id: nil) +
' ' + h(p), class: 'form--label-with-check-box')
end %>
<%= hidden_field_tag('type[project_ids][]', '', id: nil) %>
</fieldset>
<% end %>
</div>
</div>
<div class="grid-block">
<div class="grid-content medium-6">
<fieldset class="form--fieldset -collapsible collapsed" id="type_attribute_visibility">
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><%= I18n.t('label_form_configuration')%></legend>
<div style="display: none;">
<fieldset class="form--fieldset -collapsible" id="type_attribute_visibility">
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);">
<%= I18n.t('label_form_configuration')%>
</legend>
<div>
<p><%= I18n.t('text_form_configuration') %></p>
<table class="attributes-table">
<thead>
<tr>
<td><%= I18n.t('label_attribute') %></td>
<td><%= I18n.t('label_default') %></td>
<td><%= I18n.t('label_active') %></td>
<td><%= I18n.t('label_always_visible') %></td>
</tr>
</thead>
@ -88,13 +72,20 @@ See doc/COPYRIGHT.rdoc for more details.
<%
attributes = ::TypesHelper
.work_package_form_attributes(merge_date: true)
.reject { |name, attr| not name =~ /custom_field_/ and attr[:required] }
.reject { |name, attr|
# display all custom fields don't display required fields without a default
not name =~ /custom_field_/ and (attr[:required] and not attr[:has_default])
}
keys = attributes.keys.sort_by do |name|
translated_attribute_name(name, attributes[name])
end
%>
<% attributes.each do |name, attr| %>
<% keys.each do |name| %>
<% attr = attributes[name] %>
<tr>
<td>
<%= label "type_attribute_visibility_#{name}",
attr[:display_name] || attr_translate(name),
translated_attribute_name(name, attr),
value: "type_attribute_visibility[#{name}]",
class: 'form--label' %>
</td>
@ -124,6 +115,31 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
</div>
<div class="grid-block">
<div class="grid-content medium-6">
<% if @projects.any? %>
<fieldset class="form--fieldset -collapsible" id="type_project_ids">
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);">
<%= l(:label_project_plural) %>
</legend>
<div>
<div class="form--fieldset-control">
<span class="form--fieldset-control-container">
(<%= check_all_links 'type_project_ids' %>)
</span>
</div>
<%= project_nested_ul(@projects) do |p|
content_tag('label',
check_box_tag('type[project_ids][]', p.id, controller.type.projects.include?(p), id: nil) +
' ' + h(p), class: 'form--label-with-check-box')
end %>
<%= hidden_field_tag('type[project_ids][]', '', id: nil) %>
</div>
</fieldset>
<% end %>
</div>
</div>
<div class="grid-block">
<%= styled_button_tag l(controller.type.new_record? ? :button_create : :button_save),
class: '-highlight -with-icon icon-checkmark' %>

@ -34,7 +34,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= render partial: 'form', locals: { f: f,
project: @project } %>
<hr class="form--separator">
<hr class="form--separator" />
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
<% end %>

@ -35,7 +35,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= render partial: 'versions/form', locals: { f: f,
project: @project } %>
<hr class="form--separator">
<hr class="form--separator" />
<%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-checkmark' %>
<% end %>

@ -47,7 +47,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= render partial: 'attachments/form' %>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l(:button_save), class: 'button -highlight -with-icon icon-checkmark' %>
<%= preview_link preview_project_wiki_path(@project, @page), 'wiki_form-preview' %>
<%= wikitoolbar_for 'content_text' %>

@ -73,7 +73,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= render partial: 'attachments/form', f: f %>
<hr class="form--separator">
<hr class="form--separator" />
<%= f.button l(:button_save), class: 'button -highlight -with-icon icon-checkmark' %>
<%= preview_link preview_project_wiki_index_path(@project), 'wiki_form-preview' %>

@ -0,0 +1,9 @@
coverage:
ignore:
- spec/factories/.*
- vendor/bundle/.*
status:
patch: false
project:
default: {}
comment: off

@ -68,11 +68,11 @@ end
Redmine::MenuManager.map :account_menu do |menu|
menu.push :administration,
{ controller: '/admin', action: 'projects' },
html: { class: 'hidden-for-mobile'},
html: { class: 'hidden-for-mobile' },
if: Proc.new { User.current.admin? }
menu.push :my_account,
{ controller: '/my', action: 'account' },
html: { class: 'hidden-for-mobile'},
html: { class: 'hidden-for-mobile' },
if: Proc.new { User.current.logged? }
menu.push :logout, :signout_path,
if: Proc.new { User.current.logged? }
@ -167,6 +167,11 @@ Redmine::MenuManager.map :admin_menu do |menu|
html: { class: 'server_authentication icon2 icon-flag' },
if: proc { !OpenProject::Configuration.disable_password_login? }
menu.push :announcements,
{ controller: '/announcements', action: 'edit' },
caption: 'Announcement',
html: { class: 'icon2 icon-news' }
menu.push :plugins,
{ controller: '/admin', action: 'plugins' },
last: true,

@ -40,6 +40,11 @@ en:
plugins:
no_results_title_text: There are currently no plugins available.
announcements:
show_until: Show until
is_active: currently displayed
is_inactive: currently not displayed
auth_sources:
index:
no_results_content_title: There are currently no authentication modes.
@ -209,6 +214,8 @@ en:
activerecord:
attributes:
announcements:
show_until: "Display until"
attachment:
downloads: "Downloads"
file: "File"
@ -834,6 +841,7 @@ en:
indefinite_expiration: "Never"
label_account: "Account"
label_active: "Active"
label_activity: "Activity"
label_add_edit_translations: "Add and edit translations"
label_add_another_file: "Add another file"
@ -853,6 +861,7 @@ en:
label_all_time: "all time"
label_all_words: "All words"
label_always_visible: "Always displayed"
label_announcement: "Announcement"
label_and_its_subprojects: "%{value} and its subprojects"
label_api_access_key: "API access key"
label_api_access_key_created_on: "API access key created %{value} ago"
@ -1769,7 +1778,8 @@ en:
text_form_configuration: >
You can customize which attributes will be displayed by default
in forms for work packages of this type.
Fields that are required cannot be customized and will always be shown.
Fields that are required but don't have a default value cannot be customized.
They will always be shown.
text_caracters_maximum: "%{count} characters maximum."
text_caracters_minimum: "Must be at least %{count} characters long."
text_comma_separated: "Multiple values allowed (comma separated)."
@ -2050,7 +2060,7 @@ en:
tooltip:
attribute_visibility:
default: Show if not empty
visible: Always show
visible: Show even if empty
hidden: Hide by default
queries:

@ -374,7 +374,7 @@ en:
label_column_multiselect: "Combined dropdown field: Select with arrow keys, confirm selection with enter, delete with backspace"
message_error_during_bulk_delete: An error occurred while trying to delete work packages.
message_successful_bulk_delete: Successfully deleted work packages.
message_successful_show_in_fullscreen: "Click here to open this work package in fullscreen view"
message_successful_show_in_fullscreen: "Click here to open this work package in fullscreen view."
no_value: "No value"
inline_create:
title: 'Click here to add a new work package to this list'
@ -382,9 +382,8 @@ en:
header: 'New Work Package'
header_with_parent: 'New work package (Child of %{type} #%{id})'
no_results:
title: No work packages to display
description_html: |
<p>Either none have been created or all work packages are filtered out.</p>
title: No work packages to display.
description: Either none have been created or all work packages are filtered out.
property_groups:
details: "Details"
people: "People"

@ -208,7 +208,6 @@ OpenProject::Application.routes.draw do
get 'identifier', action: 'identifier'
patch 'identifier', action: 'update_identifier'
match 'copy_project_from_(:coming_from)' => 'copy_projects#copy_project', via: :get, as: :copy_from,
constraints: { coming_from: /(admin|settings)/ }
match 'copy_from_(:coming_from)' => 'copy_projects#copy', via: :post, as: :copy,
@ -288,7 +287,6 @@ OpenProject::Application.routes.draw do
end
resources :work_packages, only: [] do
collection do
get '/report/:detail' => 'work_packages/reports#report_details'
get '/report' => 'work_packages/reports#report'
@ -357,6 +355,7 @@ OpenProject::Application.routes.draw do
scope 'admin' do
match '/projects' => 'admin#projects', via: :get, as: :admin_projects
resource :announcements, only: [:edit, :update]
resources :enumerations
resources :groups do

@ -0,0 +1,28 @@
require Rails.root.join('db', 'migrate', 'migration_utils', 'migration_squasher').to_s
require 'open_project/plugins/migration_mapping'
# This migration aggregates the migrations detailed in the MIGRATION_FILES
class AggregatedAnnouncementsMigrations < ActiveRecord::Migration
MIGRATION_FILES = <<-MIGRATIONS
001_create_announcements.rb
20121114100640_index_on_announcements.rb
MIGRATIONS
OLD_PLUGIN_NAME = 'redmine_announcements'
def up
migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
Migration::MigrationSquasher.squash(migration_names) do
create_table :announcements do |t|
t.text :text
t.date :show_until
t.boolean :active, default: false
t.timestamps
end
add_index :announcements, [:show_until, :active]
end
end
def down
drop_table :announcements
end
end

@ -475,7 +475,7 @@ If you need to restart the server (for example after a configuration change), do
We heard that `bower install` can fail, if your server is behind a firewall which does not allow `git://` URLs. The error looks like this:
```
bower openproject-ui_components#with-bower ECMDERR Failed to execute "git ls-remote --tags --heads git://github.com/opf/openproject-ui_components.git", exit code of #128
ECMDERR Failed to execute "git ls-remote --tags --heads git://github.com/finnlabs/angular-modal.git", exit code of #128
Additional error details:
fatal: unable to connect to github.com:

@ -35,16 +35,15 @@ angular.module('openproject.uiComponents',
$rootScope.I18n = I18n;
}]);
export var configModule = angular.module('openproject.config', []);
angular.module(
'openproject.services', [
'openproject.uiComponents',
'openproject.helpers',
'openproject.workPackages.config',
'openproject.workPackages.helpers',
'openproject.api',
'angular-cache',
'openproject.filters'
]);
export var opServicesModule = angular.module('openproject.services', [
'openproject.uiComponents',
'openproject.helpers',
'openproject.workPackages.config',
'openproject.workPackages.helpers',
'openproject.api',
'angular-cache',
'openproject.filters'
]);
angular.module('openproject.helpers', ['openproject.services']);
angular
.module('openproject.models', [
@ -78,7 +77,7 @@ angular.module('openproject.timelines.directives', [
]);
// work packages
angular.module('openproject.workPackages', [
export const opWorkPackagesModule = angular.module('openproject.workPackages', [
'openproject.workPackages.activities',
'openproject.workPackages.controllers',
'openproject.workPackages.filters',
@ -98,24 +97,22 @@ angular.module('openproject.workPackages.filters', [
'openproject.workPackages.helpers'
]);
angular.module('openproject.workPackages.config', []);
angular.module(
'openproject.workPackages.controllers', [
'openproject.models',
'openproject.viewModels',
'openproject.workPackages.helpers',
'openproject.services',
'openproject.workPackages.config',
'openproject.layout',
'btford.modal'
]);
export const wpControllersModule = angular.module('openproject.workPackages.controllers', [
'openproject.models',
'openproject.viewModels',
'openproject.workPackages.helpers',
'openproject.services',
'openproject.workPackages.config',
'openproject.layout',
'btford.modal'
]);
angular.module('openproject.workPackages.models', []);
angular.module(
'openproject.workPackages.directives', [
'openproject.uiComponents',
'openproject.services',
'openproject.workPackages.services',
'openproject.workPackages.models'
]);
export const wpDirectivesModule = angular.module('openproject.workPackages.directives', [
'openproject.uiComponents',
'openproject.services',
'openproject.workPackages.services',
'openproject.workPackages.models'
]);
angular.module('openproject.workPackages.tabs', []);
angular.module('openproject.workPackages.activities', []);

@ -52,6 +52,9 @@ export default class WorkPackageResource extends HalResource {
wp.form = $q.when(resource);
wp.id = 'new-' + Date.now();
// Set update link to form
wp.$links.update = resource.$links.self;
deferred.resolve(wp);
})
.catch(deferred.reject);
@ -125,42 +128,30 @@ export default class WorkPackageResource extends HalResource {
delete plain.updatedAt;
var deferred = $q.defer();
this.getForm()
// Always resolve form to the latest form
// This way, we won't have to actively reset it.
this.form = this.$links.update(this.$source);
this.form
.catch(deferred.reject)
.then(form => {
var plainPayload = form.payload.$plain();
var schema = form.$embedded.schema;
angular.forEach(plain, (value, key) => {
if (typeof(schema[key]) === 'object' && schema[key]['writable'] === true) {
plainPayload[key] = value;
}
});
// Override the current schema with
// the changes from API
this.schema = form.$embedded.schema;
angular.forEach(plainPayload._links, (_value, key) => {
if (this[key] && typeof(schema[key]) === 'object' && schema[key]['writable'] === true) {
var value = this[key].href === 'null' ? null : this[key].href;
plainPayload._links[key] = {href: value};
}
});
// Merge attributes from form with resource
var payload = this.mergeWithForm(form);
return this.saveResource(plainPayload)
this.saveResource(payload)
.then(workPackage => {
angular.extend(this, workPackage);
wpCacheService.updateWorkPackageList([this]);
deferred.resolve(this);
})
.catch((error) => {
deferred.reject(error);
})
.finally(() => {
// Restore the form for subsequent saves
// e.g., due to changes in lockVersion.
// Not needed for inline create.
if (!this.isNew) {
this.form = null;
}
});
}).catch((error) => {
deferred.reject(error);
});
});
return deferred.promise;
@ -185,6 +176,30 @@ export default class WorkPackageResource extends HalResource {
return this.$links.updateImmediately(payload);
}
private mergeWithForm(form) {
var plainPayload = form.payload.$source;
var schema = form.$embedded.schema;
// Merge embedded properties from form payload
// Do not use properties on this, since they may be incomplete
// e.g., when switching to a type that requires a custom field.
angular.forEach(plainPayload, (_value, key) => {
if (typeof(schema[key]) === 'object' && schema[key]['writable'] === true) {
plainPayload[key] = this[key];
}
});
// Merged linked properties from form payload
angular.forEach(plainPayload._links, (_value, key) => {
if (this[key] && typeof(schema[key]) === 'object' && schema[key]['writable'] === true) {
var value = this[key].href === 'null' ? null : this[key].href;
plainPayload._links[key] = {href: value};
}
});
return plainPayload;
}
}
function wpResource(_$q_:ng.IQService,
@ -201,7 +216,7 @@ function wpResource(_$q_:ng.IQService,
angular
.module('openproject.api')
.service('WorkPackageResource', [
.factory('WorkPackageResource', [
'$q',
'apiWorkPackages',
'wpCacheService',

@ -2,7 +2,10 @@
<div class="notification-box--content">
<p>
<span>{{::content.message}}</span>
<a ng-click="showFullScreen()" ng-if="displayFullScreenLink">{{::I18n.t('js.work_packages.message_successful_show_in_fullscreen')}}</a>
<a ng-click="content.link.target()"
ng-if="!!content.link"
ng-bind="content.link.text">
</a>
</p>
<div data-ng-switch="content.type" data-ng-if="typeable()">
<div data-ng-switch-when="upload">

@ -1,4 +1,4 @@
//-- copyright
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
@ -24,27 +24,18 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(I18n, $timeout,$state,loadingIndicator,ConfigurationService) {
// ++
function notificationBox($timeout, I18n, ConfigurationService) {
var notificationBoxController = function(scope, element) {
scope.uploadCount = 0;
scope.show = false;
scope.I18n = I18n;
scope.currentState = $state.current.name;
scope.canBeHidden = function() {
return scope.content.uploads.length > 5;
};
scope.displayFullScreenLink = ($state.current.name.indexOf("work-packages.show") == -1 && scope.content.type === "success");
scope.showFullScreen = function(){
scope.remove();
loadingIndicator.mainPage = $state.go.apply($state, ["work-packages.show.activity", $state.params]);
};
scope.removable = function() {
return scope.content.type !== 'upload';
};
@ -83,10 +74,14 @@ module.exports = function(I18n, $timeout,$state,loadingIndicator,ConfigurationSe
return {
restrict: 'E',
replace: true,
templateUrl: '/templates/components/notification-box.html',
templateUrl: '/components/common/notification-box/notification-box.directive.html',
scope: {
content: '='
},
link: notificationBoxController
};
};
}
angular
.module('openproject.uiComponents')
.directive('notificationBox', notificationBox);

@ -0,0 +1,132 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
function notifications($rootScope) {
var createNotification = function (message) {
if (typeof message === 'string') {
return {message: message};
}
return message;
},
createSuccessNotification = function (message) {
return _.extend(createNotification(message), {type: 'success'});
},
createWarningNotification = function (message) {
return _.extend(createNotification(message), {type: 'warning'});
},
createErrorNotification = function (message, errors) {
return _.extend(createNotification(message), {
type: 'error',
errors: errors || []
});
},
createNoticeNotification = function (message) {
return _.extend(createNotification(message), {type: ''});
},
createWorkPackageUploadNotification = function (message, uploads) {
if (!uploads) {
throw new Error('Cannot create an upload notification without uploads!');
}
return _.extend(createNotification(message), {
type: 'upload',
uploads: uploads
});
},
broadcast = function (event, data) {
$rootScope.$broadcast(event, data);
},
currentNotifications = [],
notificationAdded = function (newNotification) {
var toRemove = currentNotifications.slice(0);
_.each(toRemove, function (existingNotification) {
if (newNotification.type === 'success' || newNotification.type === 'error') {
remove(existingNotification);
}
});
currentNotifications.push(newNotification);
},
notificationRemoved = function (removedNotification) {
_.remove(currentNotifications, function (element) {
return element === removedNotification;
});
},
clearNotifications = function () {
currentNotifications.forEach(function (notification) {
remove(notification);
});
};
$rootScope.$on('notification.remove', function (_e, notification) {
notificationRemoved(notification);
});
$rootScope.$on('notifications.clearAll', function () {
clearNotifications();
});
// public
var add = function (message) {
var notification = createNotification(message);
broadcast('notification.add', notification);
notificationAdded(notification);
return notification;
},
addError = function (message, errors) {
return add(createErrorNotification(message, errors));
},
addWarning = function (message) {
return add(createWarningNotification(message));
},
addSuccess = function (message) {
return add(createSuccessNotification(message));
},
addNotice = function (message) {
return add(createNoticeNotification(message));
},
addWorkPackageUpload = function (message, uploads) {
return add(createWorkPackageUploadNotification(message, uploads));
},
remove = function (notification) {
broadcast('notification.remove', notification);
};
return {
add: add,
remove: remove,
addError: addError,
addWarning: addWarning,
addSuccess: addSuccess,
addNotice: addNotice,
addWorkPackageUpload: addWorkPackageUpload
};
}
angular
.module('openproject.services')
.factory('NotificationsService', notifications);

@ -1,4 +1,4 @@
//-- copyright
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
@ -24,33 +24,31 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
// ++
describe('NotificationsService', function(){
'use strict';
describe('NotificationsService', function () {
var NotificationsService,
$rootScope;
$rootScope;
beforeEach(module('openproject.services'));
beforeEach(inject(function(_$rootScope_, _NotificationsService_) {
beforeEach(angular.mock.module('openproject.services'));
beforeEach(angular.mock.inject(function (_$rootScope_, _NotificationsService_) {
$rootScope = _$rootScope_;
NotificationsService = _NotificationsService_;
}));
it('should be able to create notifications', function() {
it('should be able to create notifications', function () {
var notification = NotificationsService.add('message');
expect(notification).to.eql({ message: 'message' });
expect(notification).to.eql({message: 'message'});
});
it('should be able to create warnings', function() {
it('should be able to create warnings', function () {
var notification = NotificationsService.addWarning('warning!');
expect(notification).to.eql({ message: 'warning!', type: 'warning' });
expect(notification).to.eql({message: 'warning!', type: 'warning'});
});
it('should be able to create error messages with errors', function() {
it('should be able to create error messages with errors', function () {
var notification = NotificationsService.addError('a super cereal error', ['fooo', 'baarr']);
expect(notification).to.eql({
message: 'a super cereal error',
@ -59,7 +57,7 @@ describe('NotificationsService', function(){
});
});
it('should be able to create error messages with only a message', function() {
it('should be able to create error messages with only a message', function () {
var notification = NotificationsService.addError('a super cereal error');
expect(notification).to.eql({
message: 'a super cereal error',
@ -68,7 +66,7 @@ describe('NotificationsService', function(){
});
});
it('should be able to create upload messages with uploads', function() {
it('should be able to create upload messages with uploads', function () {
var notification = NotificationsService.addWorkPackageUpload('uploading...', [0, 1, 2]);
expect(notification).to.eql({
message: 'uploading...',
@ -77,13 +75,13 @@ describe('NotificationsService', function(){
});
});
it('should throw an Error if trying to create an upload without uploads', function() {
expect(function() {
it('should throw an Error if trying to create an upload without uploads', function () {
expect(function () {
NotificationsService.addWorkPackageUpload('themUploads');
}).to.throw(Error);
});
it('sends a broadcast on rootScope upon adding', function() {
it('sends a broadcast on rootScope upon adding', function () {
sinon.spy($rootScope, '$broadcast');
NotificationsService.add('very important');
@ -91,53 +89,53 @@ describe('NotificationsService', function(){
expect($rootScope.$broadcast).to.have.been.calledWith('notification.add');
});
it('sends a broadcast on rootScope upon removal', function() {
it('sends a broadcast on rootScope upon removal', function () {
sinon.spy($rootScope, '$broadcast');
NotificationsService.remove({ message: 'blubs', type: 'success' });
NotificationsService.remove({message: 'blubs', type: 'success'});
expect($rootScope.$broadcast).to.have.been.calledWith('notification.remove');
});
it('sends a broadcast to remove the first notification upon adding a second success notification',
function() {
function () {
sinon.spy($rootScope, '$broadcast');
sinon.spy($rootScope, '$broadcast');
var firstNotification = NotificationsService.add('blubs');
var firstNotification = NotificationsService.add('blubs');
NotificationsService.addSuccess('blubs2');
NotificationsService.addSuccess('blubs2');
expect($rootScope.$broadcast).to.have.been.calledWith('notification.remove',
firstNotification);
});
expect($rootScope.$broadcast).to.have.been.calledWith('notification.remove',
firstNotification);
});
it('sends a broadcast to remove the first notification upon adding a second error notification',
function() {
function () {
sinon.spy($rootScope, '$broadcast');
sinon.spy($rootScope, '$broadcast');
var firstNotification = NotificationsService.add('blubs');
var firstNotification = NotificationsService.add('blubs');
NotificationsService.addError('blubs2');
NotificationsService.addError('blubs2');
expect($rootScope.$broadcast).to.have.been.calledWith('notification.remove',
firstNotification);
});
expect($rootScope.$broadcast).to.have.been.calledWith('notification.remove',
firstNotification);
});
it('does not send a broadcast upon the second error/success ' +
'if the notification has already been removed',
function() {
'if the notification has already been removed',
function () {
var firstNotification = NotificationsService.add('blubs');
$rootScope.$broadcast('notification.remove', firstNotification);
var firstNotification = NotificationsService.add('blubs');
$rootScope.$broadcast('notification.remove', firstNotification);
sinon.spy($rootScope, '$broadcast');
sinon.spy($rootScope, '$broadcast');
NotificationsService.addError('blubs2');
NotificationsService.addError('blubs2');
expect($rootScope.$broadcast).not.to.have.been.calledWith('notification.remove',
firstNotification);
expect($rootScope.$broadcast).not.to.have.been.calledWith('notification.remove',
firstNotification);
});
});
});

@ -22,7 +22,7 @@
class="{{action.icon}}">
<a role="menuitem" href="" ng-click="triggerContextMenuAction(action.icon, action.link)">
<i ng-class="['icon-action-menu', 'icon-' + action.icon]"></i>
<span ng-bind="I18n.t('js.button_' + action.icon)"/>
<span ng-bind="action.text"/>
</a>
</li>
</ul>

@ -83,9 +83,10 @@ describe('workPackageContextMenu', () => {
update: '/work_packages/123/edit',
move: '/work_packages/move/new?ids%5B%5D=123',
};
var workPackage = Factory.build('PlanningElement', {
$links: actionLinks
});
var workPackage = Factory.build('PlanningElement');
workPackage.$source = { _links: actionLinks };
workPackage.$links = actionLinks;
var directListElements;
beforeEach(angular.mock.inject((_I18n_) => {
@ -125,9 +126,9 @@ describe('workPackageContextMenu', () => {
var actionLinks = {
'delete': '/work_packages/bulk',
};
var workPackage = Factory.build('PlanningElement', {
$links: actionLinks
});
var workPackage = Factory.build('PlanningElement');
workPackage.$source = { _links: actionLinks };
workPackage.$links = actionLinks;
beforeEach(() => {
$rootScope.rows = [];

@ -92,17 +92,13 @@ function WorkPackageFieldService($q, $http, $filter, I18n, WorkPackagesHelper,
var attrVisibility = getVisibility(workPackage, field);
var notRequired = !isRequired(workPackage, field);
var notRequired = !isRequired(workPackage, field) || hasDefault(workPackage, field);
var empty = isEmpty(workPackage, field);
var visible = attrVisibility == 'visible'; // always show
var hidden = attrVisibility == 'hidden'; // never show
// !hidden && !visible => show if not empty
// not hidden and not visible => show if not empty (default)
if (workPackage.isNew === true) {
return notRequired && hidden;
} else {
return notRequired && !visible && (empty || hidden);
}
return notRequired && !visible && (empty || hidden);
}
function getVisibility(workPackage, field) {
@ -231,6 +227,14 @@ function WorkPackageFieldService($q, $http, $filter, I18n, WorkPackagesHelper,
return schema.props[field].required;
}
function hasDefault(workPackage, field) {
var schema = getSchema(workPackage);
if (_.isUndefined(schema.props[field])) {
return false;
}
return schema.props[field].hasDefault;
}
function isEmbedded(workPackage, field) {
return !_.isUndefined(workPackage.embedded[field]);
}

@ -34,6 +34,13 @@ angular
$urlMatcherFactoryProvider.strictMode(false);
var panels = {
get overview() {
return {
url: '/overview',
template: '<overview-panel work-package="workPackage"></overview-panel>'
};
},
get watchers() {
return {
url: '/watchers',
@ -180,12 +187,7 @@ angular
}
}
})
.state('work-packages.list.details.overview', {
url: '/overview',
templateUrl: '/templates/work_packages/tabs/overview.html',
controller: 'DetailsTabOverviewController',
controllerAs: 'vm',
})
.state('work-packages.list.details.overview', panels.overview)
.state('work-packages.list.details.activity', panels.activity)
.state('work-packages.list.details.activity.details', panels.activityDetails)
.state('work-packages.list.details.relations', panels.relations)

@ -72,7 +72,6 @@
rows="rows"
query="query"
group-by="query.groupBy"
group-by-column="groupByColumn"
display-sums="query.displaySums"
resource="resource"
activation-callback="openWorkPackageInFullView(id, force)">

@ -26,17 +26,28 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
angular
.module('openproject.workPackages.controllers')
.controller('WorkPackageShowController', WorkPackageShowController);
function WorkPackageShowController($scope, $rootScope, $state, workPackage, I18n,
RELATION_TYPES, RELATION_IDENTIFIERS, $q, WorkPackagesHelper, PathHelper, UsersHelper,
WorkPackageService, CommonRelationsHandler,
ChildrenRelationsHandler, ParentRelationsHandler, WorkPackagesOverviewService,
WorkPackageFieldService, EditableFieldsState, WorkPackagesDisplayHelper, NotificationsService,
WorkPackageAuthorization, PERMITTED_MORE_MENU_ACTIONS, HookService, $window,
WorkPackageAttachmentsService, AuthorisationService, inplaceEditAll) {
function WorkPackageShowController($scope,
$rootScope,
$state,
$window,
$q,
PERMITTED_MORE_MENU_ACTIONS,
RELATION_TYPES,
RELATION_IDENTIFIERS,
workPackage,
I18n,
WorkPackagesHelper,
PathHelper,
UsersHelper,
WorkPackageService,
CommonRelationsHandler,
ChildrenRelationsHandler,
ParentRelationsHandler,
EditableFieldsState,
WorkPackageAuthorization,
HookService,
AuthorisationService,
inplaceEditAll) {
$scope.editAll = inplaceEditAll;
$scope.canEdit = EditableFieldsState.canEdit;
@ -116,7 +127,7 @@ function WorkPackageShowController($scope, $rootScope, $state, workPackage, I18n
};
var authorization = new WorkPackageAuthorization($scope.workPackage);
$scope.permittedActions = angular.extend(getPermittedActions(authorization, PERMITTED_MORE_MENU_ACTIONS),
getPermittedPluginActions(authorization));
getPermittedPluginActions(authorization));
$scope.actionsAvailable = Object.keys($scope.permittedActions).length > 0;
// END stuff copied from details toolbar directive...
@ -154,7 +165,7 @@ function WorkPackageShowController($scope, $rootScope, $state, workPackage, I18n
$scope.workPackage = workPackage;
$scope.isWatched = workPackage.links.hasOwnProperty('unwatch');
$scope.displayWatchButton = workPackage.links.hasOwnProperty('unwatch') ||
workPackage.links.hasOwnProperty('watch');
workPackage.links.hasOwnProperty('watch');
// watchers
if(workPackage.links.watchers) {
@ -191,8 +202,8 @@ function WorkPackageShowController($scope, $rootScope, $state, workPackage, I18n
RELATION_TYPES[key])
).then(function(relations) {
var relationsHandler = new CommonRelationsHandler(workPackage,
relations,
RELATION_IDENTIFIERS[key]);
relations,
RELATION_IDENTIFIERS[key]);
$scope[key] = relationsHandler;
});
}
@ -208,7 +219,7 @@ function WorkPackageShowController($scope, $rootScope, $state, workPackage, I18n
// Toggle early to avoid delay.
$scope.isWatched = !$scope.isWatched;
WorkPackageService.toggleWatch($scope.workPackage)
.then(function() { refreshWorkPackage() }, outputError);
.then(function() { refreshWorkPackage() }, outputError);
};
$scope.canViewWorkPackageWatchers = function() {
@ -224,11 +235,11 @@ function WorkPackageShowController($scope, $rootScope, $state, workPackage, I18n
function getFocusAnchorLabel(tab, workPackage) {
var tabLabel = I18n.t('js.work_packages.tabs.' + tab),
params = {
tab: tabLabel,
type: workPackage.props.type,
subject: workPackage.props.subject
};
params = {
tab: tabLabel,
type: workPackage.props.type,
subject: workPackage.props.subject
};
return I18n.t('js.label_work_package_details_you_are_here', params);
}
@ -237,62 +248,8 @@ function WorkPackageShowController($scope, $rootScope, $state, workPackage, I18n
$state.current.url.replace(/\//, ''),
$scope.workPackage
);
// Stuff copied from DetailsTabOverviewController
var vm = this;
vm.groupedFields = [];
vm.hideEmptyFields = true;
vm.workPackage = $scope.workPackage;
vm.shouldHideGroup = function(group) {
return WorkPackagesDisplayHelper.shouldHideGroup(vm.hideEmptyFields,
vm.groupedFields,
group,
vm.workPackage);
};
vm.isFieldHideable = WorkPackagesDisplayHelper.isFieldHideable;
vm.getLabel = WorkPackagesDisplayHelper.getLabel;
vm.isSpecified = WorkPackagesDisplayHelper.isSpecified;
vm.hasNiceStar = WorkPackagesDisplayHelper.hasNiceStar;
vm.showToggleButton = WorkPackagesDisplayHelper.showToggleButton;
vm.filesExist = false;
activate();
WorkPackageAttachmentsService.hasAttachments(vm.workPackage).then(function(bool) {
vm.filesExist = bool;
});
function activate() {
$scope.$watch('workPackage.schema', function(schema) {
if (schema) {
WorkPackagesDisplayHelper.setFocus();
vm.workPackage = $scope.workPackage;
}
});
vm.groupedFields = WorkPackagesOverviewService.getGroupedWorkPackageOverviewAttributes();
$scope.$watchCollection('vm.workPackage.form', function() {
var schema = WorkPackageFieldService.getSchema(vm.workPackage);
var otherGroup = _.find(vm.groupedFields, {groupName: 'other'});
otherGroup.attributes = [];
_.forEach(schema.props, function(prop, propName) {
if (propName.match(/^customField/)) {
otherGroup.attributes.push(propName);
}
});
otherGroup.attributes.sort(function(a, b) {
var getLabel = function(field) {
return vm.getLabel(vm.workPackage, field);
};
var left = getLabel(a).toLowerCase(),
right = getLabel(b).toLowerCase();
return left.localeCompare(right);
});
});
$scope.$on('workPackageUpdatedInEditor', function() {
NotificationsService.addSuccess(I18n.t('js.notice_successful_update'));
});
}
}
angular
.module('openproject.workPackages.controllers')
.controller('WorkPackageShowController', WorkPackageShowController);

@ -59,61 +59,7 @@
<div class="work-packages--split-view">
<div class="work-packages--left-panel">
<div class="work-packages--panel-inner">
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">
<!-- {{ I18n.t('js.label_description') }} -->
{{type.props.name}} #{{workPackage.props.id}}
</h3>
</div>
</div>
<div class="single-attribute wiki">
<wp-field field-name="'description'"></wp-field>
</div>
</div>
<div ng-repeat="group in vm.groupedFields" ng-hide="vm.shouldHideGroup(group.groupName)" class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
ng-bind="I18n.t('js.work_packages.property_groups.' + group.groupName)"></h3>
</div>
<div class="attributes-group--header-toggle">
<panel-expander tabindex="-1" ng-if="vm.showToggleButton() && $first"
collapsed="vm.hideEmptyFields"
expand-text="{{ I18n.t('js.label_show_attributes') }}"
collapse-text="{{ I18n.t('js.label_hide_attributes') }}">
</panel-expander>
</div>
</div>
<div class="attributes-key-value">
<div
ng-hide="vm.hideEmptyFields && vm.isFieldHideable(vm.workPackage, field)"
ng-if="vm.isSpecified(vm.workPackage, field)"
ng-repeat-start="field in group.attributes"
class="attributes-key-value--key"
id="attributes-key-value--key-{{field}}">
{{vm.getLabel(vm.workPackage, field)}}
<span class="required" ng-if="vm.hasNiceStar(vm.workPackage, field)"> *</span>
</div>
<div
ng-hide="vm.hideEmptyFields && vm.isFieldHideable(vm.workPackage, field)"
ng-if="vm.isSpecified(vm.workPackage, field)"
ng-repeat-end
class="attributes-key-value--value-container">
<wp-field field-name="field"></wp-field>
</div>
</div>
</div>
<work-package-attachments edit data-ng-show="!vm.hideEmptyFields || vm.filesExist" work-package="vm.workPackage"></work-package-attachments>
<wp-single-view></wp-single-view>
<edit-actions-bar></edit-actions-bar>
</div>
</div>

@ -1,4 +1,4 @@
//-- copyright
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
@ -24,13 +24,13 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/* jshint expr: true */
/* globals WebKitBlobBuilder */
// ++
declare const WebKitBlobBuilder:any;
describe('workPackageAttachmentsService', function() {
'use strict';
var WorkPackageAttachmentsService, $httpBackend;
var WorkPackageAttachmentsService;
var $httpBackend;
// mock me a work package
// TODO: remove that hyperagent.js nonsense asap
@ -61,9 +61,9 @@ describe('workPackageAttachmentsService', function() {
}
};
beforeEach(module('openproject.workPackages'));
beforeEach(angular.mock.module('openproject.workPackages'));
beforeEach(inject(function(_WorkPackageAttachmentsService_, _$httpBackend_){
beforeEach(angular.mock.inject(function(_WorkPackageAttachmentsService_, _$httpBackend_){
WorkPackageAttachmentsService = _WorkPackageAttachmentsService_;
$httpBackend = _$httpBackend_;
}));

@ -0,0 +1,109 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {opServicesModule} from '../../../angular-modules.ts';
function wpAttachmentsService($q, $timeout, $http, Upload, I18n, NotificationsService) {
var upload = (workPackage, files) => {
var uploadPath = workPackage.$links.addAttachment.$link.href;
var uploads = _.map(files, (file:any) => {
var options = {
url: uploadPath,
fields: {
metadata: {
fileName: file.name,
description: file.description
}
},
file: file
};
return Upload.upload(options);
});
// notify the user
var message = I18n.t('js.label_upload_notification', {
id: workPackage.id,
subject: workPackage.subject
});
var notification = NotificationsService.addWorkPackageUpload(message, uploads);
var allUploadsDone = $q.defer();
$q.all(uploads).then(function () {
$timeout(function () { // let the notification linger for a bit
NotificationsService.remove(notification);
allUploadsDone.resolve();
}, 700);
}, function (err) {
allUploadsDone.reject(err);
});
return allUploadsDone.promise;
},
load = function (workPackage, reload) {
var path = workPackage.$links.attachments.$link.href,
attachments = $q.defer();
$http.get(path, {cache: !reload}).success(function (response) {
attachments.resolve(response._embedded.elements);
}).error(function (err) {
attachments.reject(err);
});
return attachments.promise;
},
remove = function (fileOrAttachment) {
var removal = $q.defer();
if (angular.isObject(fileOrAttachment._links)) {
var path = fileOrAttachment._links.self.href;
$http.delete(path).success(function () {
removal.resolve(fileOrAttachment);
}).error(function (err) {
removal.reject(err);
});
} else {
removal.resolve(fileOrAttachment);
}
return removal.promise;
},
hasAttachments = function (workPackage) {
var existance = $q.defer();
load(workPackage).then(function (attachments:any) {
existance.resolve(attachments.length > 0);
});
return existance.promise;
};
return {
upload: upload,
remove: remove,
load: load,
hasAttachments: hasAttachments
};
}
opServicesModule.factory('WorkPackageAttachmentsService', wpAttachmentsService);

@ -1,19 +1,19 @@
<span class="wp-table--cell-span"
ng-switch="vm.displayType"
wp-field="vm.workPackage"
field-name="vm.attribute">
ng-switch="$ctrl.displayType"
wp-field="$ctrl.workPackage"
field-name="$ctrl.attribute">
<progress-bar ng-switch-when="Percent"
progress="vm.displayText"
progress="$ctrl.displayText"
width="80px">
</progress-bar>
<span ng-switch-when="SelfLink" title="{{ vm.displayText }}">
<a ng-href="{{ vm.displayLink }}">{{ vm.displayText }}</a>
<span ng-switch-when="SelfLink" title="{{ $ctrl.displayText }}">
<a ng-href="{{ $ctrl.displayLink }}">{{ $ctrl.displayText }}</a>
</span>
<span ng-switch-default
title="{{ vm.displayText }}"
ng-class="{ 'work-package--placeholder' : vm.displayText == '-' }">
{{ vm.displayText }}
title="{{ $ctrl.displayText }}"
ng-class="{ 'work-package--placeholder' : $ctrl.displayText == '-' }">
{{ $ctrl.displayText }}
</span>
</span>

@ -45,7 +45,7 @@ describe('wpDisplayAttr directive', () => {
beforeEach(angular.mock.inject(($rootScope, $compile) => {
var html = `
<wp-display-attr object="workPackage" schema="schema" attribute="attribute">
<wp-display-attr work-package="workPackage" schema="schema" attribute="attribute">
</wp-display-attr>
`;

@ -33,7 +33,7 @@ export default class WorkPackageDisplayAttributeController {
public displayType:string;
public displayLink:string;
public attribute:string;
public object:any;
public workPackage:any;
public schema:HalResource;
constructor(protected $scope:ng.IScope,
@ -42,7 +42,7 @@ export default class WorkPackageDisplayAttributeController {
protected WorkPackagesHelper:any) {
this.displayText = I18n.t('js.work_packages.placeholders.default');
$scope.$watch('vm.object.' + this.attribute, () => {
$scope.$watch('$ctrl.workPackage.' + this.attribute, () => {
this.updateAttribute();
});
}
@ -55,7 +55,7 @@ export default class WorkPackageDisplayAttributeController {
else if (this.attribute === 'id') {
// Show a link to the work package for the ID
this.displayType = 'SelfLink';
this.displayLink = this.PathHelper.workPackagePath(this.object.id);
this.displayLink = this.PathHelper.workPackagePath(this.workPackage.id);
}
else {
this.displayType = this.schema[this.attribute].type;
@ -64,22 +64,22 @@ export default class WorkPackageDisplayAttributeController {
protected updateAttribute() {
this.schema.$load().then(() => {
if (this.object.isNew && this.attribute === 'id') {
if (this.workPackage.isNew && this.attribute === 'id') {
this.displayText = 'text';
this.displayText = '';
return;
}
if (!this.object[this.attribute]) {
if (!this.workPackage[this.attribute]) {
this.displayText = this.I18n.t('js.work_packages.placeholders.default');
return;
}
this.setDisplayType();
var text = this.object[this.attribute].value ||
this.object[this.attribute].name ||
this.object[this.attribute];
var text = this.workPackage[this.attribute].value ||
this.workPackage[this.attribute].name ||
this.workPackage[this.attribute];
this.displayText = this.WorkPackagesHelper.formatValue(text, this.displayType);
});
@ -94,13 +94,13 @@ function wpDisplayAttr() {
scope: {
schema: '=',
object: '=',
workPackage: '=',
attribute: '='
},
bindToController: true,
controller: WorkPackageDisplayAttributeController,
controllerAs: 'vm'
controllerAs: '$ctrl'
};
}

@ -34,6 +34,7 @@ function WorkPackageNewController($scope,
$rootScope,
$state,
$stateParams,
I18n,
PathHelper,
WorkPackagesOverviewService,
WorkPackageFieldService,
@ -75,7 +76,17 @@ function WorkPackageNewController($scope,
vm.showToggleButton = WorkPackagesDisplayHelper.showToggleButton;
vm.notifyCreation = function() {
NotificationsService.addSuccess(I18n.t('js.notice_successful_create'));
NotificationsService.addSuccess({
message: I18n.t('js.notice_successful_create'),
link: {
target: function() {
loadingIndicator.mainPage = $state.go.apply($state,
["work-packages.show.activity",
$state.params]);
},
text: I18n.t('js.work_packages.message_successful_show_in_fullscreen')
}
});
};
vm.getHeading = function() {
if (vm.parentWorkPackage !== undefined) {

@ -0,0 +1,299 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {opWorkPackagesModule} from "../../../angular-modules";
function wpSingleViewFieldService($filter,
I18n,
WorkPackagesHelper,
inplaceEditErrors) {
function getSchema(workPackage) {
return workPackage.schema;
}
function isEditable(workPackage, field) {
// no form - no editing
if (!workPackage.form) {
return false;
}
var schema = getSchema(workPackage);
// TODO: extract to strategy if new cases arise
if (field === 'date') {
// nope
return schema['startDate'].writable && schema['dueDate'].writable;
//return workPackage.schema.startDate.writable
// && workPackage.schema.dueDate.writable;
}
if(schema[field].type === 'Date') {
return true;
}
var isWritable = schema[field].writable;
// not writable if no embedded allowed values
if (isWritable && schema[field]._links && allowedValuesEmbedded(workPackage, field)) {
isWritable = getEmbeddedAllowedValues(workPackage, field).length > 0;
}
return isWritable;
}
function isSpecified(workPackage, field) {
var schema = getSchema(workPackage);
if (field === 'date') {
// kind of specified
return true;
}
return !_.isUndefined(schema[field]);
}
// under special conditions fields will be shown
// irregardless if they are empty or not
// e.g. when an error should trigger the editing state
// of an empty field after type change
function isHideable(workPackage, field) {
if (inplaceEditErrors.errors && inplaceEditErrors.errors[field]) {
return false;
}
var attrVisibility = getVisibility(workPackage, field);
var notRequired = !isRequired(workPackage, field);
var empty = isEmpty(workPackage, field);
var visible = attrVisibility == 'visible'; // always show
var hidden = attrVisibility == 'hidden'; // never show
// !hidden && !visible => show if not empty
if (workPackage.isNew === true) {
return notRequired && hidden;
} else {
return notRequired && !visible && (empty || hidden);
}
}
function getVisibility(workPackage, field) {
if (field == "date") {
return getDateVisibility(workPackage);
} else {
var schema = workPackage.schema;
var prop = schema && schema[field];
return prop && prop.visibility;
}
}
/**
* There isn't actually a 'date' field for work packages.
* There are two fields: 'start_date' and 'due_date'
* Though they are displayed together in one row, as one 'field'.
* Since the schema doesn't know any field named 'date' we
* derive the visibility for the imaginary 'date' field from
* the actual schema values of 'due_date' and 'start_date'.
*
* 'visible' > 'default' > 'hidden'
* Meaning, for instance, that if at least one field is 'visible'
* both will be shown. Even if the other is 'hidden'.
*
* Note: this is duplicated in app/views/types/_form.html.erb
*/
function getDateVisibility(workPackage) {
var a = getVisibility(workPackage, "startDate");
var b = getVisibility(workPackage, "dueDate");
var values = [a, b];
if (_.contains(values, "visible")) {
return "visible";
} else if (_.contains(values, "default")) {
return "default";
} else if (_.contains(values, "hidden")) {
return "hidden";
} else {
return undefined;
}
}
function isMilestone(workPackage) {
// TODO: this should be written as "only use the form when editing"
// otherwise always use the simple way
// currently we don't know the context in which this method is called
var formAvailable = !_.isUndefined(workPackage.form);
if (formAvailable) {
var allowedValues = workPackage.schema.type.$embedded.allowedValues;
var currentType = workPackage.$links.type.$link.href;
return _.some(allowedValues, function(allowedValue) {
return allowedValue.href === currentType &&
allowedValue.isMilestone;
});
} else {
return workPackage.type.isMilestone;
}
}
function getValue(workPackage, field) {
var payload = workPackage;
if (field === 'date') {
if(isMilestone(workPackage)) {
return payload['dueDate'];
}
return {
startDate: payload['startDate'],
dueDate: payload['dueDate']
};
}
if (!_.isUndefined(payload[field])) {
return payload[field];
}
if (isEmbedded(payload, field)) {
return payload.$embedded[field];
}
if (payload.$links[field] && payload.$links[field].$link.href !== null) {
return payload.$links[field];
}
return null;
}
function allowedValuesEmbedded(workPackage, field) {
var schema = getSchema(workPackage);
return _.isArray(schema[field]._links.allowedValues);
}
function getEmbeddedAllowedValues(workPackage, field) {
var options = [];
var schema = getSchema(workPackage);
return schema[field].$embedded.allowedValues;
}
function isRequired(workPackage, field) {
var schema = getSchema(workPackage);
if (_.isUndefined(schema[field])) {
return false;
}
return schema[field].required;
}
function isEmbedded(workPackage, field) {
return !_.isUndefined(workPackage.$embedded[field]);
}
function getLabel(workPackage, field) {
var schema = getSchema(workPackage);
if (field === 'date') {
// special case
return I18n.t('js.work_packages.properties.date');
}
return schema[field].name;
}
function isEmpty(workPackage, field) {
if (field === 'date') {
return (
getValue(workPackage, 'startDate') === null &&
getValue(workPackage, 'dueDate') === null
);
}
var value = format(workPackage, field);
if (value === null || value === '') {
return true;
}
if (value.html === '') {
return true;
}
if (field === 'spentTime' && workPackage[field] === 'PT0S') {
return true;
}
if (value.$embedded && _.isArray(value.$embedded.elements)) {
return value.$embedded.elements.length === 0;
}
return false;
}
function format(workPackage, field) {
var schema = getSchema(workPackage);
if (field === 'date') {
if(isMilestone(workPackage)) {
return workPackage['dueDate'];
}
return {
startDate: workPackage.startDate,
dueDate: workPackage.dueDate,
noStartDate: I18n.t('js.label_no_start_date'),
noEndDate: I18n.t('js.label_no_due_date')
};
}
var value = workPackage[field];
if (_.isUndefined(value)) {
return getValue(workPackage, field, true);
}
if (value === null) {
return null;
}
var fieldMapping = {
dueDate: 'date',
startDate: 'date',
createdAt: 'datetime',
updatedAt: 'datetime'
}[field] || schema[field] && schema[field].type;
switch(fieldMapping) {
case('Duration'):
var hours = moment.duration(value).asHours();
var formattedHours = $filter('number')(hours, 2);
return I18n.t('js.units.hour', { count: formattedHours });
case('Boolean'):
return value ? I18n.t('js.general_text_yes') : I18n.t('js.general_text_no');
case('Date'):
return value;
case('Float'):
return $filter('number')(value);
default:
return WorkPackagesHelper.formatValue(value, fieldMapping);
}
}
return {
isEditable: isEditable,
isRequired: isRequired,
isSpecified: isSpecified,
isHideable: isHideable,
isMilestone: isMilestone,
isEmbedded: isEmbedded,
getLabel: getLabel,
};
}
opWorkPackagesModule.service('wpSingleViewField', wpSingleViewFieldService);

@ -0,0 +1,63 @@
<div ng-if="$ctrl.workPackage" wp-edit-form="$ctrl.workPackage">
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">
{{ $ctrl.workPackage.type.name }} #{{ $ctrl.workPackage.id }}
</h3>
</div>
<div class="attributes-group--header-container-right">
<span ng-bind="$ctrl.I18n.t('js.label_last_updated_on')"/>
<op-date date-value="$ctrl.workPackage.updatedAt"></op-date>
</div>
</div>
<div
wp-edit-field="'description'"
class="single-attribute wiki">
<wp-display-attr
schema="$ctrl.workPackage.schema"
work-package="$ctrl.workPackage"
attribute="'description'">
</wp-display-attr>
</div>
</div>
<div ng-repeat="group in $ctrl.groupedFields" ng-hide="$ctrl.shouldHideGroup(group.groupName)" class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
ng-bind="$ctrl.I18n.t('js.work_packages.property_groups.' + group.groupName)"></h3>
</div>
<div class="attributes-group--header-toggle">
<panel-expander tabindex="-1" ng-if="$ctrl.wpSingleView.showToggleButton() && $first"
collapsed="$ctrl.hideEmptyFields"
expand-text="{{ $ctrl.I18n.t('js.label_show_attributes') }}"
collapse-text="{{ $ctrl.I18n.t('js.label_hide_attributes') }}">
</panel-expander>
</div>
</div>
<div class="attributes-key-value">
<div
ng-hide="$ctrl.hideEmptyFields && $ctrl.wpSingleView.isFieldHideable($ctrl.workPackage, field)"
ng-if="$ctrl.wpSingleView.isSpecified($ctrl.workPackage, field)"
ng-repeat-start="field in group.attributes" class="attributes-key-value--key">
{{$ctrl.wpSingleView.getLabel($ctrl.workPackage, field)}}
<span class="required" ng-if="$ctrl.wpSingleView.hasNiceStar($ctrl.workPackage, field)"> *</span>
</div>
<div
ng-hide="$ctrl.hideEmptyFields && $ctrl.wpSingleView.isFieldHideable($ctrl.workPackage, field)"
ng-if="$ctrl.wpSingleView.isSpecified($ctrl.workPackage, field)"
ng-repeat-end
wp-edit-field="field"
class="attributes-key-value--value-container">
<wp-display-attr schema="$ctrl.workPackage.schema" work-package="$ctrl.workPackage" attribute="field"></wp-display-attr>
</div>
</div>
</div>
<work-package-attachments edit work-package="$ctrl.workPackage" data-ng-show="!$ctrl.hideEmptyFields || $ctrl.filesExist"></work-package-attachments>
</div>

@ -0,0 +1,125 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {opWorkPackagesModule} from "../../../angular-modules";
import {scopedObservable} from "../../../helpers/angular-rx-utils";
export class WorkPackageSingleViewController {
public workPackage;
public groupedFields:any[] = [];
public hideEmptyFields:boolean = true;
public filesExist:boolean = false;
constructor(protected $scope,
protected $state,
protected loadingIndicator,
protected $stateParams,
public wpSingleView,
protected I18n,
protected wpCacheService,
protected NotificationsService,
protected WorkPackagesOverviewService,
protected WorkPackageFieldService,
protected inplaceEditAll,
protected WorkPackageAttachmentsService) {
scopedObservable($scope, wpCacheService.loadWorkPackage($stateParams.workPackageId)).subscribe(wp => {
this.workPackage = wp;
this.workPackage.schema.$load();
this.groupedFields = WorkPackagesOverviewService.getGroupedWorkPackageOverviewAttributes();
WorkPackageAttachmentsService.hasAttachments(this.workPackage).then(bool => {
this.filesExist = bool;
});
$scope.$watch('$ctrl.workPackage.schema', schema => {
if (schema) {
this.wpSingleView.setFocus();
}
});
$scope.$watchCollection('$ctrl.workPackage.form', () => {
var schema = WorkPackageFieldService.getSchema(this.workPackage);
var otherGroup:any = _.find(this.groupedFields, {groupName: 'other'});
otherGroup.attributes = [];
angular.forEach(schema.props, (prop, propName) => {
if (propName.match(/^customField/)) {
otherGroup.attributes.push(propName);
}
});
otherGroup.attributes.sort((a, b) => {
var getLabel = field => this.wpSingleView.getLabel(this.workPackage, field);
var left = getLabel(a).toLowerCase();
var right = getLabel(b).toLowerCase();
return left.localeCompare(right);
});
});
});
$scope.$on('workPackageUpdatedInEditor', () => {
NotificationsService.addSuccess({
message: I18n.t('js.notice_successful_update'),
link: {
target: () => {
loadingIndicator.mainPage = $state.go(
...["work-packages.show.activity", $state.params]);
},
text: I18n.t('js.work_packages.message_successful_show_in_fullscreen')
}
});
});
}
public shouldHideGroup(group) {
return this.wpSingleView.shouldHideGroup(
this.hideEmptyFields, this.groupedFields, group, this.workPackage);
}
}
function wpSingleViewDirective() {
return {
restrict: 'E',
templateUrl: '/components/work-packages/wp-single-view/wp-single-view.directive.html',
scope: {},
bindToController: true,
controller: WorkPackageSingleViewController,
controllerAs: '$ctrl'
};
}
opWorkPackagesModule.directive('wpSingleView', wpSingleViewDirective);

@ -0,0 +1,140 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
import {opWorkPackagesModule} from "../../../angular-modules";
function wpSingleViewService($window, $timeout, wpSingleViewField) {
// specifies unhideable (during creation)
var unhideableFields = [
'subject',
'type',
'status',
'description',
'priority'
];
var firstTimeFocused = false;
var isGroupHideable = function (groupedFields, groupName, workPackage, cb) {
if (!workPackage) {
return true;
}
var group = _.find(groupedFields, {groupName: groupName});
var isHideable = typeof cb === 'undefined' ? isFieldHideable : cb;
return group.attributes.length === 0 || _.every(group.attributes, function(field) {
return isHideable(workPackage, field);
});
},
isGroupEmpty = function (groupedFields, groupName) {
var group = _.find(groupedFields, {groupName: groupName});
return group.attributes.length === 0;
},
shouldHideGroup = function(hideEmptyActive, groupedFields, groupName, workPackage, cb) {
return hideEmptyActive && isGroupHideable(groupedFields, groupName, workPackage, cb) ||
!hideEmptyActive && isGroupEmpty(groupedFields, groupName);
},
isFieldHideable = function (workPackage, field) {
if (!workPackage) {
return true;
}
return wpSingleViewField.isHideable(workPackage, field);
},
isFieldHideableOnCreate = function(workPackage, field) {
if (!workPackage) {
return true;
}
if (!isSpecified(workPackage, field)) {
return true;
}
if (!isEditable(workPackage, field)) {
return true;
}
if (_.contains(unhideableFields, field)) {
return !wpSingleViewField.isEditable(workPackage, field);
}
if (wpSingleViewField.isRequired(workPackage, field)) {
return false;
}
return wpSingleViewField.isHideable(workPackage, field);
},
isSpecified = function (workPackage, field) {
if (!workPackage) {
return false;
}
return wpSingleViewField.isSpecified(workPackage, field);
},
isEditable = function(workPackage, field) {
return wpSingleViewField.isEditable(workPackage, field);
},
hasNiceStar = function (workPackage, field) {
if (!workPackage) {
return false;
}
return wpSingleViewField.isRequired(workPackage, field) &&
wpSingleViewField.isEditable(workPackage, field);
},
getLabel = function (workPackage, field) {
if (!(workPackage && typeof field === 'string')) {
return '';
}
return wpSingleViewField.getLabel(workPackage, field);
},
setFocus = function() {
if (!firstTimeFocused) {
firstTimeFocused = true;
$timeout(function() {
// TODO: figure out a better way to fix the wp table columns bug
// where arrows are misplaced when not resizing the window
angular.element($window).trigger('resize');
angular.element('.work-packages--details--subject .focus-input').focus();
});
}
},
showToggleButton = function () {
return true;
};
return {
isGroupHideable: isGroupHideable,
isGroupEmpty: isGroupEmpty,
shouldHideGroup: shouldHideGroup,
isFieldHideable: isFieldHideable,
isFieldHideableOnCreate: isFieldHideableOnCreate,
isSpecified: isSpecified,
isEditable: isEditable,
hasNiceStar: hasNiceStar,
getLabel: getLabel,
setFocus: setFocus,
showToggleButton: showToggleButton
};
}
opWorkPackagesModule.factory('wpSingleView', wpSingleViewService);

@ -54,6 +54,11 @@ class WorkPackageInlineCreateButtonController extends WorkPackageCreateButtonCon
}
});
// Need to reset the state when the work package is refreshed hard
$rootScope.$on('workPackagesRefreshRequired', _ => {
this.show();
});
this.apiWorkPackages.availableProjects().then(resource => {
this.canCreate = (resource && resource.total > 0);
this.availableProjects = resource.elements;
@ -71,8 +76,16 @@ class WorkPackageInlineCreateButtonController extends WorkPackageCreateButtonCon
return !this.canCreate || this.$state.includes('**.new');
}
public get projectIdentifierForCreate() {
if (this.inProjectContext) {
return this.projectIdentifier;
} else {
return this.availableProjects[0].identifier;
}
}
public addWorkPackageRow() {
this.WorkPackageResource.fromCreateForm(this.availableProjects[0].identifier).then(wp => {
this.WorkPackageResource.fromCreateForm(this.projectIdentifierForCreate).then(wp => {
this._wp = wp;
wp.inlineCreated = true;

@ -116,7 +116,7 @@ export class WorkPackageEditFieldController {
public set editable(enabled:boolean) {
this._editable = enabled;
this.$element.toggleClass('-editable', enabled);
this.$element.toggleClass('-editable', !!enabled);
}
public shouldFocus() {
@ -190,8 +190,8 @@ function wpEditField() {
scope: {
fieldName: '=wpEditField',
fieldIndex: '=fieldIndex',
columns: '=columns'
fieldIndex: '=',
columns: '='
},
require: ['^wpEditForm', 'wpEditField'],

@ -32,10 +32,13 @@ export class WorkPackageEditFormController {
public firstActiveField:string;
constructor(
protected I18n,
protected NotificationsService,
protected $q,
protected QueryService,
protected $state,
protected $rootScope,
protected loadingIndicator,
protected $timeout) {
}
@ -60,6 +63,7 @@ export class WorkPackageEditFormController {
angular.forEach(this.fields, field => field.setErrorState(false));
deferred.resolve();
this.showSaveNotification();
this.$rootScope.$emit('workPackageSaved', this.workPackage);
this.$rootScope.$emit('workPackagesRefreshInBackground');
})
@ -76,6 +80,20 @@ export class WorkPackageEditFormController {
return deferred.promise;
}
private showSaveNotification() {
var message = 'js.notice_successful_' + (this.workPackage.inlineCreated ? 'create' : 'update');
this.NotificationsService.addSuccess({
message: this.I18n.t(message),
link: {
target: _ => {
this.loadingIndicator.mainPage = this.$state.go.apply(this.$state,
["work-packages.show.activity", { workPackageId: this.workPackage.id }]);
},
text: this.I18n.t('js.work_packages.message_successful_show_in_fullscreen')
}
});
}
private handleSubmissionErrors(error:any, deferred:any) {
// Process single API errors

@ -0,0 +1,11 @@
<wp-single-view></wp-single-view>
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">{{ vm.I18n.t('js.label_latest_activity') }}</h3>
</div>
</div>
<activity-panel template="overview" work-package="workPackage"></activity-panel>
</div>

@ -26,44 +26,13 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {scopedObservable} from "../../../helpers/angular-rx-utils";
import {openprojectModule} from "../../../angular-modules";
import IScope = angular.IScope;
import WorkPackage = op.WorkPackage;
import {WorkPackageCacheService} from "../../work-packages/work-package-cache.service";
import WorkPackageResource from "../../api/api-v3/hal-resources/work-package-resource.service";
import {wpDirectivesModule} from "../../../angular-modules";
export class OverviewPanelController {
public workPackage: WorkPackageResource;
constructor($scope: IScope, $stateParams: any, private wpCacheService: WorkPackageCacheService) {
const wpId = parseInt($stateParams.workPackageId);
scopedObservable($scope, wpCacheService.loadWorkPackage(wpId)).subscribe(wp => {
this.workPackage = wp;
this.workPackage.schema.$load().then((schema) => {
// TODO use schema
});
});
}
}
function wpOverviewPanel() {
function overviewPanel(){
return {
restrict: 'E',
// scope: {
// workPackage: '=wpEditForm'
// },
templateUrl: "/components/wp-panels/overview-panel/wp-overview-panel.directive.html",
controller: OverviewPanelController,
controllerAs: '$ctrl',
bindToController: true
templateUrl: '/components/wp-panels/overview-panel/overview-panel.directive.html',
};
}
openprojectModule.directive('wpOverviewPanel', wpOverviewPanel);
wpDirectivesModule.directive('overviewPanel', overviewPanel);

@ -1,17 +0,0 @@
<div wp-edit-form="$ctrl.workPackage">
<div ng-repeat="column in columns"
class="{{column.name}}"
lang="{{column.custom_field && column.custom_field.name_locale || locale}}"
wp-edit-field="column.name">
<wp-display-attr attribute="column.name"
schema="$ctrl.workPackage.schema"
object="$ctrl.workPackage">
</wp-display-attr>
</div>
</div>

@ -26,19 +26,26 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(PERMITTED_BULK_ACTIONS, WorkPackagesTableService, UrlParamsHelper) {
angular
.module('openproject.workPackages.helpers')
.factory('WorkPackageContextMenuHelper', WorkPackageContextMenuHelper);
function WorkPackageContextMenuHelper(PERMITTED_BULK_ACTIONS, WorkPackagesTableService, HookService, UrlParamsHelper) {
function getPermittedActionLinks(workPackage, permittedActionConstants) {
var singularPermittedActions = [];
var allowedActions = getAllowedActions(workPackage.$links, permittedActionConstants);
var allowedActions = getAllowedActions(workPackage, permittedActionConstants);
angular.forEach(allowedActions, function(allowedAction) {
singularPermittedActions.push({
icon: allowedAction.icon,
link: workPackage
.$links[allowedAction.link]
.href
});
icon: allowedAction.icon,
text: allowedAction.text,
link: workPackage
.$source
._links[allowedAction.link]
.href
});
});
return singularPermittedActions;
@ -49,15 +56,16 @@ module.exports = function(PERMITTED_BULK_ACTIONS, WorkPackagesTableService, UrlP
var permittedActions = _.filter(PERMITTED_BULK_ACTIONS, function(action) {
return _.every(workPackages, function(workPackage) {
return getAllowedActions(workPackage.$links, [action]).length === 1;
return getAllowedActions(workPackage, [action]).length >= 1;
});
});
angular.forEach(permittedActions, function(permittedAction) {
bulkPermittedActions.push({
icon: permittedAction.icon,
link: getBulkActionLink(permittedAction,
workPackages)
});
icon: permittedAction.icon,
text: permittedAction.text,
link: getBulkActionLink(permittedAction,
workPackages)
});
});
return bulkPermittedActions;
@ -80,20 +88,28 @@ module.exports = function(PERMITTED_BULK_ACTIONS, WorkPackagesTableService, UrlP
return link + '?' + queryParts.join('&');
}
function getAllowedActions(links, actions) {
function getAllowedActions(workPackage, actions) {
var allowedActions = [];
angular.forEach(actions, function(action) {
if (links.hasOwnProperty(action.link)) {
if (workPackage.$links.hasOwnProperty(action.link)) {
action.text = I18n.t('js.button_' + action.icon);
allowedActions.push(action);
}
});
angular.forEach(HookService.call('workPackageTableContextMenu'), function(action) {
if (workPackage.$links.hasOwnProperty(action.link)) {
var index = action.indexBy ? action.indexBy(allowedActions) : allowedActions.length;
allowedActions.splice(index, 0, action)
}
});
return allowedActions;
}
var WorkPackageContextMenuHelper = {
getPermittedActions: function(workPackages, permittedActionConstants) {
getPermittedActions: function (workPackages, permittedActionConstants) {
if (workPackages.length === 1) {
return getPermittedActionLinks(workPackages[0], permittedActionConstants);
} else if (workPackages.length > 1) {
@ -103,4 +119,4 @@ module.exports = function(PERMITTED_BULK_ACTIONS, WorkPackagesTableService, UrlP
};
return WorkPackageContextMenuHelper;
};
}

@ -33,29 +33,30 @@ angular
function wpGroupHeader() {
return {
restrict: 'A',
link: function(scope) {
compile: function() {
return {
pre: function(scope) {
scope.currentGroup = scope.row.groupName;
scope.currentGroup = scope.row.groupName;
scope.currentGroupObject = _.find(scope.resource.groups, function(o) {
return o.value === scope.row.groupName;
});
scope.currentGroupObject = _.find(scope.resource.groups, function(o) {
var value = o.value == null ? '' : o.value;
return value === scope.row.groupName;
});
pushGroup(scope.currentGroup);
if (scope.currentGroupObject && scope.currentGroupObject.value === null) {
scope.currentGroupObject.value = I18n.t('js.placeholders.default');
}
scope.toggleCurrentGroup = function() {
scope.groupExpanded[scope.currentGroup] = !scope.groupExpanded[scope.currentGroup];
};
pushGroup(scope.currentGroup);
function pushGroup(group) {
if (scope.groupExpanded[group] === undefined) {
scope.groupExpanded[group] = true;
}
}
}
scope.toggleCurrentGroup = function() {
scope.groupExpanded[scope.currentGroup] = !scope.groupExpanded[scope.currentGroup];
};
function pushGroup(group) {
if (scope.groupExpanded[group] === undefined) {
scope.groupExpanded[group] = true;
}
}
}
};
}

@ -1,6 +1,6 @@
<div class="generic-table--container work-package-table--container"
ng-class="{ '-with-footer': displaySums }">
<div class="generic-table--results-container" ng-if="rows.length">
<div class="generic-table--results-container">
<table interactive-table class="keyboard-accessible-list generic-table">
<colgroup>
<col highlight-col />
@ -37,13 +37,24 @@
</thead>
<tbody>
<!-- Empty row notification -->
<tr id="empty-row-notification" ng-if="!rows.length">
<td colspan="{{ numTableColumns }}">
<span>
<i class="icon-info1 icon-context"></i>
<strong ng-bind="text.noResults"></strong>
<span ng-bind="text.noResultsDescription"></span>
</span>
</td>
</tr>
<!-- Group headers -->
<tr wp-group-header
ng-repeat-start="row in rows track by row.object.id"
ng-repeat-start="row in rows track by row.object.id+row.groupName"
ng-if="!!groupByColumn &&
($first || row.groupName !== rows[$index-1].groupName)"
ng-show="row.groupName != undefined"
ng-class="{
group: true,
open: groupExpanded[currentGroup],
@ -51,7 +62,7 @@
keyboard_hover: true
}"
id="group-header-{{ row.groupName }}">
<td colspan="{{ columns.length + 2 - (!!hideWorkPackageDetails * 1) }}">
<td colspan="{{ numTableColumns }}">
<div ng-class="[
'expander',
'icon-context',
@ -138,7 +149,7 @@
ng-class="{ '-short': column.name == 'id' }">
<wp-display-attr attribute="column.name"
schema="row.object.schema"
object="row.object"
work-package="row.object"
ng-class="[row.level > 0 && column.name == 'subject' && 'icon-context icon-arrow-right5 icon-small']">
</wp-display-attr>
</td>
@ -167,7 +178,7 @@
<td ng-repeat="column in columns">
<wp-display-attr attribute="column.name"
schema="resource.sumsSchema"
object="currentGroupObject.sums">
work-package="currentGroupObject.sums">
</wp-display-attr>
</td>
</tr>
@ -176,7 +187,7 @@
<!-- Inline create button -->
<tr class="wp-inline-create-button-row">
<!-- Add 2 to the colspan attr because of the id and the checkbox columns -->
<td colspan="{{ columns.length + 2 }}">
<td colspan="{{ numTableColumns }}">
<wp-inline-create-button
project-identifier="projectIdentifier"
query="query"
@ -198,7 +209,7 @@
<wp-display-attr class="generic-table--footer-outer"
attribute="column.name"
schema="resource.sumsSchema"
object="resource.totalSums">
work-package="resource.totalSums">
</wp-display-attr>
</td>
</tr>
@ -207,12 +218,4 @@
<div class="generic-table--header-background"></div>
<div class="generic-table--footer-background" ng-if="sumsLoaded()"></div>
</div>
<div class="generic-table--no-results-container" ng-if="!rows.length">
<i class="icon-info1"></i>
<h2 class="generic-table--no-results-title">
{{ text.noResults }}
</h2>
<div class="generic-table--no-results-description"
ng-bind-html="text.noResultsDescription"></div>
</div>
</div>

@ -30,7 +30,14 @@ angular
.module('openproject.workPackages.directives')
.directive('wpTable', wpTable);
function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages, $state){
function wpTable(
WorkPackagesTableService,
WorkPackageService,
$window,
PathHelper,
apiWorkPackages,
$state
){
return {
restrict: 'E',
replace: true,
@ -52,6 +59,9 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
link: function(scope, element) {
var activeSelectionBorderIndex;
// Total columns = all available columns + id + checkbox
scope.numTableColumns = scope.columns.length + 2;
scope.workPackagesTableData = WorkPackagesTableService.getWorkPackagesTableData();
scope.workPackagePath = PathHelper.workPackagePath;
@ -60,9 +70,7 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
event.pageY -= topMenuHeight;
};
// groupings
scope.grouped = scope.groupByColumn !== undefined;
scope.groupExpanded = {};
applyGrouping();
scope.$watchCollection('columns', function() {
// force Browser rerender
@ -88,6 +96,8 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
if (scope.displaySums) {
fetchSumsSchema();
}
applyGrouping();
});
scope.$watch('displaySums', function(sumsToBeDisplayed) {
@ -97,6 +107,14 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
}
});
function applyGrouping() {
if (scope.groupByColumn != scope.workPackagesTableData.groupByColumn) {
scope.groupByColumn = scope.workPackagesTableData.groupByColumn;
scope.grouped = scope.groupByColumn !== undefined;
scope.groupExpanded = {};
}
}
function fetchTotalSums() {
apiWorkPackages
// TODO: use the correct page offset and per page options
@ -125,17 +143,6 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
WorkPackagesTableService.setCheckedStateForAllRows(scope.rows, state);
};
var groupableColumns = WorkPackagesTableService.getGroupableColumns();
scope.$watch('query.groupBy', function(groupBy) {
if (scope.columns) {
var groupByColumnIndex = groupableColumns.map(function(column){
return column.name;
}).indexOf(groupBy);
scope.groupByColumn = groupableColumns[groupByColumnIndex];
}
});
// Thanks to http://stackoverflow.com/a/880518
function clearSelection() {
if(document.selection && document.selection.empty) {
@ -173,6 +180,11 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
}
scope.selectWorkPackage = function(row, $event) {
// 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
WorkPackageService.cache().put('preselectedWorkPackageId', row.object.id);
if ($event.target.type != 'checkbox') {
var currentRowCheckState = row.checked;
var multipleChecked = mulipleRowsChecked();
@ -221,7 +233,7 @@ function WorkPackagesTableController($scope, $rootScope) {
sumFor: I18n.t('js.label_sum_for'),
allWorkPackages: I18n.t('js.label_all_work_packages'),
noResults: I18n.t('js.work_packages.no_results.title'),
noResultsDescription: I18n.t('js.work_packages.no_results.description_html')
noResultsDescription: I18n.t('js.work_packages.no_results.description')
};
$scope.$watch('workPackagesTableData.allRowsChecked', function(checked) {

@ -116,8 +116,15 @@ function WorkPackagesTableService($filter, QueryService, WorkPackagesTableHelper
return workPackagesTableData.groupBy;
},
getGroupByColumn: function() {
return workPackagesTableData.groupByColumn;
},
setGroupBy: function(groupBy) {
var groupableColumns = workPackagesTableData.groupableColumns;
workPackagesTableData.groupBy = groupBy;
workPackagesTableData.groupByColumn = _.find(groupableColumns, { name: groupBy });
},
removeRow: function(row) {

@ -74,11 +74,6 @@ angular.module('openproject.services')
'fields[]': 'status_id',
'operators[status_id]': 'o'
})
.service('NotificationsService', [
'I18n',
'$rootScope',
require('./notifications-service.js')
])
.service('ApiNotificationsService', [
'NotificationsService',
'ApiHelper',

@ -1,130 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(I18n, $rootScope) {
'use strict';
// private
var createNotification = function(message) {
if(typeof message === 'string') {
return { message: message };
}
return message;
},
createSuccessNotification = function(message) {
return _.extend(createNotification(message), { type: 'success' });
},
createWarningNotification = function(message) {
return _.extend(createNotification(message), { type: 'warning' });
},
createErrorNotification = function(message, errors) {
return _.extend(createNotification(message), {
type: 'error',
errors: errors || []
});
},
createNoticeNotification = function(message) {
return _.extend(createNotification(message), { type: '' });
},
createWorkPackageUploadNotification = function(message, uploads) {
if(!uploads) {
throw new Error('Cannot create an upload notification without uploads!');
}
return _.extend(createNotification(message), {
type: 'upload',
uploads: uploads
});
},
broadcast = function(event, data) {
$rootScope.$broadcast(event, data);
},
currentNotifications = [],
notificationAdded = function(newNotification) {
var toRemove = currentNotifications.slice(0);
_.each(toRemove, function(existingNotification) {
if (newNotification.type === 'success' || newNotification.type === 'error') {
remove(existingNotification);
}
});
currentNotifications.push(newNotification);
},
notificationRemoved = function(removedNotification) {
_.remove(currentNotifications, function(element) {
return element === removedNotification;
});
},
clearNotifications = function() {
_.remove(currentNotifications);
};
$rootScope.$on('notification.remove', function(_e, notification) {
notificationRemoved(notification);
});
$rootScope.$on('notifications.clearAll', function() {
clearNotifications();
});
// public
var add = function(message) {
var notification = createNotification(message);
broadcast('notification.add', notification);
notificationAdded(notification);
return notification;
},
addError = function(message, errors) {
return add(createErrorNotification(message, errors));
},
addWarning = function(message) {
return add(createWarningNotification(message));
},
addSuccess = function(message) {
return add(createSuccessNotification(message));
},
addNotice = function(message) {
return add(createNoticeNotification(message));
},
addWorkPackageUpload = function(message, uploads) {
return add(createWorkPackageUploadNotification(message, uploads));
},
remove = function(notification) {
broadcast('notification.remove', notification);
};
return {
add: add,
remove: remove,
addError: addError,
addWarning: addWarning,
addSuccess: addSuccess,
addNotice: addNotice,
addWorkPackageUpload: addWorkPackageUpload
};
};

@ -1,73 +0,0 @@
<hr>
Start
<hr>
<wp-overview-panel></wp-overview-panel>
<hr>
Ende
<hr>
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">
{{ type.props.name }} #{{ workPackage.props.id }}
</h3>
</div>
<div class="attributes-group--header-container-right">
<span ng-bind="I18n.t('js.label_last_updated_on')"/>
<op-date date-value="workPackage.props.updatedAt"></op-date>
</div>
</div>
<div class="single-attribute wiki">
<wp-field field-name="'description'"></wp-field>
</div>
</div>
<div ng-repeat="group in vm.groupedFields" ng-hide="vm.shouldHideGroup(group.groupName)" class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
ng-bind="I18n.t('js.work_packages.property_groups.' + group.groupName)"></h3>
</div>
<div class="attributes-group--header-toggle">
<panel-expander tabindex="-1" ng-if="vm.showToggleButton() && $first"
collapsed="vm.hideEmptyFields"
expand-text="{{ I18n.t('js.label_show_attributes') }}"
collapse-text="{{ I18n.t('js.label_hide_attributes') }}">
</panel-expander>
</div>
</div>
<div class="attributes-key-value">
<div
ng-hide="vm.hideEmptyFields && vm.isFieldHideable(vm.workPackage, field)"
ng-if="vm.isSpecified(vm.workPackage, field)"
ng-repeat-start="field in group.attributes" class="attributes-key-value--key">
{{vm.getLabel(vm.workPackage, field)}}
<span class="required" ng-if="vm.hasNiceStar(vm.workPackage, field)"> *</span>
</div>
<div
ng-hide="vm.hideEmptyFields && vm.isFieldHideable(vm.workPackage, field)"
ng-if="vm.isSpecified(vm.workPackage, field)"
ng-repeat-end
class="attributes-key-value--value-container">
<wp-field field-name="field"></wp-field>
</div>
</div>
</div>
<work-package-attachments edit work-package="vm.workPackage" data-ng-show="!vm.hideEmptyFields || vm.filesExist"></work-package-attachments>
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">{{ I18n.t('js.label_latest_activity') }}</h3>
</div>
</div>
<activity-panel template="overview" work-package="workPackage"></activity-panel>
</div>

@ -96,14 +96,6 @@ angular.module('openproject.uiComponents')
.directive('zoomSlider', ['I18n', require('./zoom-slider-directive')])
.directive('clickNotification', ['$timeout','NotificationsService', require('./click-notification-directive')])
.directive('notifications', [require('./notifications-directive')])
.directive('notificationBox', [
'I18n',
'$timeout',
'$state',
'loadingIndicator',
'ConfigurationService',
require('./notification-box-directive')
])
.directive('uploadProgress', [require('./upload-progress-directive')])
.directive('attachmentIcon', [require('./attachment-icon-directive')])
.filter('ancestorsExpanded', require('./filters/ancestors-expanded-filter'))

@ -1,104 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(
$scope,
WorkPackagesOverviewService,
WorkPackageFieldService,
EditableFieldsState,
inplaceEditAll,
WorkPackageDisplayHelper,
NotificationsService,
WorkPackageAttachmentsService
) {
var vm = this;
vm.groupedFields = [];
vm.hideEmptyFields = true;
vm.workPackage = $scope.workPackage;
//Show all attributes in Edit-Mode
$scope.$watch(function(){
return inplaceEditAll.state;
},function(newState, oldState){
if(newState !== oldState){
vm.hideEmptyFields = !newState;
}
});
vm.shouldHideGroup = function(group) {
return WorkPackageDisplayHelper.shouldHideGroup(vm.hideEmptyFields,
vm.groupedFields,
group,
vm.workPackage);
};
vm.isFieldHideable = WorkPackageDisplayHelper.isFieldHideable;
vm.getLabel = WorkPackageDisplayHelper.getLabel;
vm.isSpecified = WorkPackageDisplayHelper.isSpecified;
vm.hasNiceStar = WorkPackageDisplayHelper.hasNiceStar;
vm.showToggleButton = WorkPackageDisplayHelper.showToggleButton;
vm.filesExist = false;
activate();
WorkPackageAttachmentsService.hasAttachments(vm.workPackage).then(function(bool) {
vm.filesExist = bool;
});
function activate() {
$scope.$watch('workPackage.schema', function(schema) {
if (schema) {
WorkPackageDisplayHelper.setFocus();
vm.workPackage = $scope.workPackage;
}
});
vm.groupedFields = WorkPackagesOverviewService.getGroupedWorkPackageOverviewAttributes();
$scope.$watchCollection('vm.workPackage.form', function() {
var schema = WorkPackageFieldService.getSchema(vm.workPackage);
var otherGroup = _.find(vm.groupedFields, {groupName: 'other'});
otherGroup.attributes = [];
_.forEach(schema.props, function(prop, propName) {
if (propName.match(/^customField/)) {
otherGroup.attributes.push(propName);
}
});
otherGroup.attributes.sort(function(a, b) {
var getLabel = function(field) {
return vm.getLabel(vm.workPackage, field);
};
var left = getLabel(a).toLowerCase(),
right = getLabel(b).toLowerCase();
return left.localeCompare(right);
});
});
$scope.$on('workPackageUpdatedInEditor', function() {
NotificationsService.addSuccess(I18n.t('js.notice_successful_update'));
});
}
};

@ -35,17 +35,6 @@ angular.module('openproject.workPackages.controllers')
.constant('USER_TYPE', 'user')
.constant('TIME_ENTRY_TYPE', 'time_entry')
.constant('USER_FIELDS', ['assignee', 'author', 'responsible'])
.controller('DetailsTabOverviewController', [
'$scope',
'WorkPackagesOverviewService',
'WorkPackageFieldService',
'EditableFieldsState',
'inplaceEditAll',
'WorkPackagesDisplayHelper',
'NotificationsService',
'WorkPackageAttachmentsService',
require('./details-tab-overview-controller')
])
.constant('ADD_WATCHER_SELECT_INDEX', -1)
.constant('RELATION_TYPES', {
relatedTo: 'Relation::Relates',

@ -70,7 +70,7 @@ module.exports = function(
scope.rejectedFiles = [];
scope.size = ConversionService.fileSize;
scope.hasRightToUpload = !!(workPackage.links.addAttachment || workPackage.isNew);
scope.hasRightToUpload = !!(workPackage.$links.addAttachment || workPackage.isNew);
var currentlyRemoving = [];
scope.remove = function(file) {

@ -49,10 +49,6 @@ angular.module('openproject.workPackages.helpers')
link: 'delete'
}
])
.service('WorkPackageContextMenuHelper', ['PERMITTED_BULK_ACTIONS',
'WorkPackagesTableService', 'UrlParamsHelper', require(
'./work-package-context-menu-helper')
])
.factory('WorkPackagesHelper', ['TimezoneService', 'currencyFilter',
'CustomFieldHelper', require('./work-packages-helper')
])

@ -31,16 +31,18 @@ module.exports = function(WorkPackageFieldService, $window, $timeout) {
// specifies unhideable (during creation)
var unhideableFields = [
'subject',
'type',
'status',
'description',
'priority'
'description'
];
var firstTimeFocused = false;
var isGroupHideable = function (groupedFields, groupName, workPackage, cb) {
if (!workPackage) {
return true;
}
if (groupName === 'details') {
return false; // never hide details to keep show all button arround
}
var group = _.find(groupedFields, {groupName: groupName});
var isHideable = typeof cb === 'undefined' ? isFieldHideable : cb;
return group.attributes.length === 0 || _.every(group.attributes, function(field) {
@ -78,10 +80,6 @@ module.exports = function(WorkPackageFieldService, $window, $timeout) {
return !WorkPackageFieldService.isEditable(workPackage, field);
}
if (WorkPackageFieldService.isRequired(workPackage, field)) {
return false;
}
return WorkPackageFieldService.isHideable(workPackage, field);
},
isSpecified = function (workPackage, field) {

@ -52,14 +52,4 @@ angular.module('openproject.workPackages.services')
.service('WorkPackagesOverviewService', [
'WORK_PACKAGE_ATTRIBUTES',
require('./work-packages-overview-service')
])
.service('WorkPackageAttachmentsService', [
'Upload', // 'Upload' is provided by ngFileUpload
'PathHelper',
'I18n',
'NotificationsService',
'$q',
'$timeout',
'$http',
require('./work-package-attachments-service')
]);

@ -1,105 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(Upload, PathHelper, I18n, NotificationsService, $q, $timeout, $http) {
'use strict';
var upload = function(workPackage, files) {
var uploadPath = workPackage.links.addAttachment.url();
// for file in files build some promises, create a notification per WP,
// notify the noticiation (wat?) about progress
var uploads = _.map(files, function(file) {
var options = {
url: uploadPath,
fields: {
metadata: {
fileName: file.name,
description: file.description
}
},
file: file
};
return Upload.upload(options);
});
// notify the user
var message = I18n.t('js.label_upload_notification', {
id: workPackage.props.id,
subject: workPackage.props.subject
});
var notification = NotificationsService.addWorkPackageUpload(message, uploads);
var allUploadsDone = $q.defer();
$q.all(uploads).then(function() {
$timeout(function() { // let the notification linger for a bit
NotificationsService.remove(notification);
allUploadsDone.resolve();
}, 700);
}, function(err) {
allUploadsDone.reject(err);
});
return allUploadsDone.promise;
},
load = function(workPackage, reload) {
var path = workPackage.links.attachments.url(),
attachments = $q.defer();
$http.get(path, { cache: !reload }).success(function(response) {
attachments.resolve(response._embedded.elements);
}).error(function(err) {
attachments.reject(err);
});
return attachments.promise;
},
remove = function(fileOrAttachment) {
var removal = $q.defer();
if (angular.isObject(fileOrAttachment._links)) {
var path = fileOrAttachment._links.self.href;
$http.delete(path).success(function() {
removal.resolve(fileOrAttachment);
}).error(function(err) {
removal.reject(err);
});
} else {
removal.resolve(fileOrAttachment);
}
return removal.promise;
},
hasAttachments = function(workPackage) {
var existance = $q.defer();
load(workPackage).then(function(attachments) {
existance.resolve(attachments.length > 0);
});
return existance.promise;
};
return {
upload: upload,
remove: remove,
load: load,
hasAttachments: hasAttachments
};
};

@ -102,9 +102,9 @@ describe('WorkPackageContextMenuHelper', function() {
}
};
var workPackage = Factory.build('PlanningElement', {
$links: actionLinks
});
var workPackage = Factory.build('PlanningElement');
workPackage.$source = { _links : actionLinks };
workPackage.$links = actionLinks;
describe('when an array with a single work package is passed as an argument', function() {
var workPackages = new Array(workPackage);
@ -126,13 +126,15 @@ describe('WorkPackageContextMenuHelper', function() {
});
describe('when more than one work package is passed as an argument', function() {
var anotherWorkPackage = Factory.build('PlanningElement', {
$links: {
update: {
href: '/work_packages/234/edit'
}
}
});
var anotherWorkPackage = Factory.build('PlanningElement');
anotherWorkPackage.$source = {
_links: {
update: {
href: '/work_packages/234/edit'
}
}
};
anotherWorkPackage.$links = { update: '/work_packages/234/edit' };
var workPackages = [anotherWorkPackage, workPackage];
beforeEach(inject(function(_WorkPackagesTableService_) {

@ -128,8 +128,6 @@ module.exports = {
'angular-context-menu': 'angular-context-menu/dist/angular-context-menu.js',
'mousetrap': 'mousetrap/mousetrap.js',
'hyperagent': 'hyperagent/dist/hyperagent',
'openproject-ui_components':
'openproject-ui_components/app/assets/javascripts/angular/ui-components-app',
'ngFileUpload': 'ng-file-upload/ng-file-upload'
}, pluginAliases)
},

@ -65,7 +65,7 @@ module API
end
def model_required?
true
false
end
private

@ -41,6 +41,7 @@ module API
value_representer:,
link_factory:,
required: true,
has_default: false,
writable: true,
visibility: nil,
current_user: nil)
@ -50,6 +51,7 @@ module API
super(type: type,
name: name,
required: required,
has_default: has_default,
writable: writable,
visibility: visibility,
current_user: current_user)

@ -34,12 +34,13 @@ module API
module Decorators
class PropertySchemaRepresenter < ::API::Decorators::Single
def initialize(
type:, name:, required: true, writable: true,
type:, name:, required: true, has_default: false, writable: true,
visibility: nil, current_user: nil
)
@type = type
@name = name
@required = required
@has_default = has_default
@writable = writable
@visibility = visibility || 'default'
@ -49,6 +50,7 @@ module API
attr_accessor :type,
:name,
:required,
:has_default,
:writable,
:visibility,
:min_length,
@ -58,6 +60,7 @@ module API
property :type, exec_context: :decorator
property :name, exec_context: :decorator
property :required, exec_context: :decorator
property :has_default, exec_context: :decorator
property :writable, exec_context: :decorator
property :visibility, exec_context: :decorator
property :min_length, exec_context: :decorator

@ -30,11 +30,32 @@
module API
module Decorators
class Schema < Single
module InstanceMethods
module_function
def call_or_use(object)
if object.respond_to? :call
instance_exec(&object)
else
object
end
end
def call_or_translate(object, rep_class = self.class.represented_class)
if object.respond_to? :call
instance_exec(&object)
else
rep_class.human_attribute_name(object)
end
end
end
class << self
def schema(property,
type:,
name_source: property,
required: true,
has_default: false,
writable: -> { represented.writable?(property) },
visibility: nil,
min_length: nil,
@ -51,6 +72,7 @@ module API
type: type,
name: name,
required: call_or_use(required),
has_default: call_or_use(has_default),
writable: call_or_use(writable),
visibility: call_or_use(visibility))
schema.min_length = min_length
@ -61,7 +83,13 @@ module API
},
writeable: false,
if: show_if,
required: required
required: required,
has_default: has_default,
name_source: lambda {
API::Decorators::Schema::InstanceMethods
.call_or_translate name_source,
self.represented_class
}
end
def schema_with_allowed_link(property,
@ -69,6 +97,7 @@ module API
name_source: property,
href_callback:,
required: true,
has_default: false,
writable: -> { represented.writable?(property) },
visibility: nil,
show_if: true)
@ -81,6 +110,7 @@ module API
type: type,
name: call_or_translate(name_source),
required: call_or_use(required),
has_default: call_or_use(has_default),
writable: call_or_use(writable),
visibility: call_or_use(visibility))
@ -91,7 +121,13 @@ module API
representer
},
if: show_if,
required: required
required: required,
has_default: has_default,
name_source: lambda {
API::Decorators::Schema::InstanceMethods
.call_or_translate name_source,
self.represented_class
}
end
def schema_with_allowed_collection(property,
@ -103,6 +139,7 @@ module API
value_representer:,
link_factory:,
required: true,
has_default: false,
writable: -> { represented.writable?(property) },
visibility: nil,
show_if: true)
@ -118,6 +155,7 @@ module API
value_representer: value_representer,
link_factory: -> (value) { instance_exec(value, &link_factory) },
required: call_or_use(required),
has_default: call_or_use(has_default),
writable: call_or_use(writable),
visibility: call_or_use(visibility))
@ -126,7 +164,13 @@ module API
representer
},
if: show_if,
required: required
required: required,
has_default: has_default,
name_source: lambda {
API::Decorators::Schema::InstanceMethods
.call_or_translate name_source,
self.represented_class
}
end
def represented_class
@ -139,7 +183,11 @@ module API
end
end
include InstanceMethods
attr_reader :form_embedded
def initialize(represented, current_user:, form_embedded: false)
@form_embedded = form_embedded
super(represented, current_user: current_user)
@ -147,22 +195,6 @@ module API
private
def call_or_use(object)
if object.respond_to? :call
instance_exec(&object)
else
object
end
end
def call_or_translate(object)
if object.respond_to? :call
instance_exec(&object)
else
self.class.represented_class.human_attribute_name(object)
end
end
def _type
'Schema'
end

@ -37,6 +37,8 @@ module API
module Projects
class ProjectCollectionRepresenter < ::API::Decorators::UnpaginatedCollection
element_decorator ::API::V3::Projects::ProjectRepresenter
self.to_eager_load = ::API::V3::Projects::ProjectRepresenter.to_eager_load
end
end
end

@ -78,6 +78,8 @@ module API
def _type
'Project'
end
self.to_eager_load = [:enabled_modules, :project_type]
end
end
end

@ -237,6 +237,7 @@ module API
type: TYPE_MAP[custom_field.field_format],
name_source: -> (*) { custom_field.name },
required: custom_field.is_required,
has_default: (not custom_field.default_value.nil?),
writable: true,
min_length: (custom_field.min_length if custom_field.min_length > 0),
max_length: (custom_field.max_length if custom_field.max_length > 0),

@ -38,7 +38,9 @@ module API
end
get do
available_projects = WorkPackage.allowed_target_projects_on_create(current_user)
available_projects = WorkPackage
.allowed_target_projects_on_create(current_user)
.includes(Projects::ProjectCollectionRepresenter.to_eager_load)
self_link = api_v3_paths.available_projects_on_create
Projects::ProjectCollectionRepresenter.new(available_projects,
self_link,

@ -38,7 +38,9 @@ module API
end
get do
available_projects = WorkPackage.allowed_target_projects_on_move(current_user)
available_projects = WorkPackage
.allowed_target_projects_on_move(current_user)
.includes(Projects::ProjectCollectionRepresenter.to_eager_load)
self_link = api_v3_paths.available_projects_on_edit(@work_package.id)
Projects::ProjectCollectionRepresenter.new(available_projects,
self_link,

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

Loading…
Cancel
Save