Merge pull request #3701 from opf/16364-fullscreen-edit

16364 Fullscreen edit
pull/3721/merge
Stefan Botzenhart 9 years ago
commit 6b39490e37
  1. 5
      app/assets/stylesheets/_work_packages_show_view_overwrite.scss
  2. 12
      app/assets/stylesheets/layout/_work_package.sass
  3. 7
      config/locales/js-en.yml
  4. 85
      features/issues/issue_edit.feature
  5. 4
      features/search/search.feature
  6. 6
      features/step_definitions/error_steps.rb
  7. 5
      features/step_definitions/work_package_changesets_steps.rb
  8. 31
      features/step_definitions/work_package_steps.rb
  9. 4
      features/support/paths.rb
  10. 10
      features/work_packages/changesets_on_show.feature
  11. 36
      features/work_packages/editable_fields.feature
  12. 9
      features/work_packages/error_on_update.feature
  13. 70
      features/work_packages/localized_log_time.feature
  14. 64
      features/work_packages/log_time_on_update.feature
  15. 3
      features/work_packages/navigate_to_edit.feature
  16. 12
      features/work_packages/preview.feature
  17. 6
      features/work_packages/switch_type.feature
  18. 67
      features/work_packages/update.feature
  19. 75
      frontend/app/api/hal-api-resource.js
  20. 64
      frontend/app/components/common/services/hal-api-resource.service.js
  21. 21
      frontend/app/components/common/services/hal-api-resource.service.test.js
  22. 6
      frontend/app/components/inplace-edit/directives/display-pane/display-pane.directive.html
  23. 105
      frontend/app/components/inplace-edit/directives/display-pane/display-pane.directive.js
  24. 2
      frontend/app/components/inplace-edit/directives/edit-pane/edit-pane.directive.html
  25. 286
      frontend/app/components/inplace-edit/directives/edit-pane/edit-pane.directive.js
  26. 8
      frontend/app/components/inplace-edit/directives/field-display/display-spent-time/display-spent-time.directive.html
  27. 36
      frontend/app/components/inplace-edit/directives/field-display/display-spent-time/display-spent-time.directive.js
  28. 28
      frontend/app/components/inplace-edit/directives/field-display/display-user/display-user.directive.html
  29. 93
      frontend/app/components/inplace-edit/directives/field-display/display-user/display-user.directive.js
  30. 13
      frontend/app/components/inplace-edit/directives/field-display/display-version/display-version.directive.html
  31. 43
      frontend/app/components/inplace-edit/directives/field-display/display-version/display-version.directive.js
  32. 12
      frontend/app/components/inplace-edit/directives/field-edit/edit-date-range/edit-date-range.directive.html
  33. 46
      frontend/app/components/inplace-edit/directives/field-edit/edit-date-range/edit-date-range.directive.js
  34. 15
      frontend/app/components/inplace-edit/directives/field-edit/edit-date/edit-date.directive.html
  35. 36
      frontend/app/components/inplace-edit/directives/field-edit/edit-date/edit-date.directive.js
  36. 15
      frontend/app/components/inplace-edit/directives/field-edit/edit-drop-down/edit-drop-down.directive.html
  37. 125
      frontend/app/components/inplace-edit/directives/field-edit/edit-drop-down/edit-drop-down.directive.js
  38. 16
      frontend/app/components/inplace-edit/directives/field-edit/edit-duration/edit-duration.directive.html
  39. 57
      frontend/app/components/inplace-edit/directives/field-edit/edit-duration/edit-duration.directive.js
  40. 3
      frontend/app/components/inplace-edit/directives/field-edit/edit-type/edit-type.directive.html
  41. 33
      frontend/app/components/inplace-edit/directives/field-edit/edit-type/edit-type.directive.js
  42. 24
      frontend/app/components/inplace-edit/directives/field-edit/edit-wiki-textarea/edit-wiki-textarea.directive.html
  43. 71
      frontend/app/components/inplace-edit/directives/field-edit/edit-wiki-textarea/edit-wiki-textarea.directive.js
  44. 2
      frontend/app/components/inplace-edit/directives/main-pane/main-pane.directive.html
  45. 16
      frontend/app/components/inplace-edit/directives/main-pane/main-pane.directive.js
  46. 7
      frontend/app/components/inplace-edit/directives/work-package-field/work-package-field.directive.html
  47. 63
      frontend/app/components/inplace-edit/directives/work-package-field/work-package-field.directive.js
  48. 113
      frontend/app/components/inplace-edit/services/inplace-edit.service.js
  49. 27
      frontend/app/components/inplace-edit/services/inplace-edit.service.test.js
  50. 75
      frontend/app/components/inplace-edit/services/work-package-field.service.js
  51. 11
      frontend/app/openproject-app.js
  52. 1
      frontend/app/templates/components/inplace_editor/display/boolean.html
  53. 1
      frontend/app/templates/components/inplace_editor/display/date.html
  54. 1
      frontend/app/templates/components/inplace_editor/display/daterange.html
  55. 1
      frontend/app/templates/components/inplace_editor/display/embedded.html
  56. 2
      frontend/app/templates/components/inplace_editor/display/text.html
  57. 1
      frontend/app/templates/components/inplace_editor/display/wiki_textarea.html
  58. 10
      frontend/app/templates/components/inplace_editor/editable/boolean.html
  59. 1
      frontend/app/templates/components/inplace_editor/editable/dropdown.html
  60. 8
      frontend/app/templates/components/inplace_editor/editable/float.html
  61. 7
      frontend/app/templates/components/inplace_editor/editable/integer.html
  62. 7
      frontend/app/templates/components/inplace_editor/editable/text.html
  63. 1
      frontend/app/templates/inplace-edit/display/fields/boolean.html
  64. 1
      frontend/app/templates/inplace-edit/display/fields/date-range.html
  65. 1
      frontend/app/templates/inplace-edit/display/fields/date.html
  66. 2
      frontend/app/templates/inplace-edit/display/fields/dynamic.html
  67. 1
      frontend/app/templates/inplace-edit/display/fields/embedded.html
  68. 0
      frontend/app/templates/inplace-edit/display/fields/spent-time.html
  69. 2
      frontend/app/templates/inplace-edit/display/fields/text.html
  70. 0
      frontend/app/templates/inplace-edit/display/fields/user.html
  71. 0
      frontend/app/templates/inplace-edit/display/fields/version.html
  72. 1
      frontend/app/templates/inplace-edit/display/fields/wiki-textarea.html
  73. 14
      frontend/app/templates/inplace-edit/edit/fields/boolean.html
  74. 0
      frontend/app/templates/inplace-edit/edit/fields/date-range.html
  75. 0
      frontend/app/templates/inplace-edit/edit/fields/date.html
  76. 1
      frontend/app/templates/inplace-edit/edit/fields/drop-down.html
  77. 0
      frontend/app/templates/inplace-edit/edit/fields/duration.html
  78. 16
      frontend/app/templates/inplace-edit/edit/fields/float.html
  79. 15
      frontend/app/templates/inplace-edit/edit/fields/integer.html
  80. 15
      frontend/app/templates/inplace-edit/edit/fields/text.html
  81. 2
      frontend/app/templates/inplace-edit/edit/fields/textarea.html
  82. 1
      frontend/app/templates/inplace-edit/edit/fields/type.html
  83. 0
      frontend/app/templates/inplace-edit/edit/fields/wiki-textarea.html
  84. 6
      frontend/app/templates/work_packages.list.details.html
  85. 10
      frontend/app/templates/work_packages.list.html
  86. 6
      frontend/app/templates/work_packages.list.new.html
  87. 26
      frontend/app/templates/work_packages.show.html
  88. 10
      frontend/app/templates/work_packages/comment_field.html
  89. 7
      frontend/app/templates/work_packages/field.html
  90. 14
      frontend/app/templates/work_packages/inplace_editor/custom/display/user.html
  91. 11
      frontend/app/templates/work_packages/inplace_editor/custom/display/version.html
  92. 9
      frontend/app/templates/work_packages/inplace_editor/custom/editable/date.html
  93. 16
      frontend/app/templates/work_packages/inplace_editor/custom/editable/dropdown.html
  94. 8
      frontend/app/templates/work_packages/inplace_editor/custom/editable/duration.html
  95. 18
      frontend/app/templates/work_packages/inplace_editor/custom/editable/wiki_textarea.html
  96. 4
      frontend/app/templates/work_packages/tabs/overview.html
  97. 1
      frontend/app/templates/work_packages/watcher_button.html
  98. 7
      frontend/app/templates/work_packages/work_package_details_toolbar.html
  99. 10
      frontend/app/templates/work_packages/work_package_edit_actions.html
  100. 1
      frontend/app/work_packages/controllers/index.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -220,6 +220,11 @@ body.controller-work_packages.action-show {
.activity-comment { .activity-comment {
margin-top: 15px; margin-top: 15px;
} }
.button.icon-edit.ng-hide {
display: block !important;
visibility: hidden;
}
} }
.nosidebar { .nosidebar {

@ -178,6 +178,18 @@
.work-packages--attachments .work-packages--attachments
margin-bottom: 25px margin-bottom: 25px
.work-packages--edit-actions
@extend .work-packages--details-toolbar
.work-packages--left-panel &
position: absolute
width: calc(60% + 7px)
left: -20px
padding: 0 20px .5rem
.edit-all-mode .work-packages--left-panel
padding-bottom: 50px
.work-package--attachments--files .work-package--attachments--files
margin-bottom: 1rem margin-bottom: 1rem

@ -163,7 +163,6 @@ en:
label_global_queries: "Shared queries" label_global_queries: "Shared queries"
label_custom_queries: "Private queries" label_custom_queries: "Private queries"
label_columns: "Columns" label_columns: "Columns"
label_click_to_enter_description: "Click to enter description..."
label_attachments: Files label_attachments: Files
label_drop_files: Drop files here label_drop_files: Drop files here
label_drop_files_hint: or click to add files label_drop_files_hint: or click to add files
@ -180,6 +179,9 @@ en:
label_successful_update: 'Successful update' label_successful_update: 'Successful update'
label_validation_error: "The work package could not be saved due to the following errors:" label_validation_error: "The work package could not be saved due to the following errors:"
placeholders:
default: '-'
text_are_you_sure: "Are you sure?" text_are_you_sure: "Are you sure?"
watchers: watchers:
@ -386,6 +388,9 @@ en:
updatedAt: "Updated on" updatedAt: "Updated on"
versionName: "Version" versionName: "Version"
version: "Version" version: "Version"
placeholders:
default: "-"
description: "Click to enter description..."
query: query:
column_names: "Columns" column_names: "Columns"
group_by: "Group results by" group_by: "Group results by"

@ -1,85 +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.
#++
Feature: Issue edit
Background:
Given there is 1 project with the following:
| identifier | omicronpersei8 |
| name | omicronpersei8 |
And I am working in project "omicronpersei8"
And the project "omicronpersei8" has the following types:
| name | position |
| Bug | 1 |
And there is a default issuepriority with:
| name | Normal |
And there is a role "member"
And the role "member" may have the following rights:
| view_work_packages |
| edit_work_packages |
| add_work_package_notes |
And there is 1 user with the following:
| login | bob|
And the user "bob" is a "member" in the project "omicronpersei8"
And there are the following issue status:
| name | is_closed | is_default |
| New | false | true |
Given the user "bob" has 1 issue with the following:
| subject | issue1 |
| description | Aioli Sali Grande |
And I am already logged in as "bob"
@javascript
Scenario: User updates an issue successfully
When I go to the page of the issue "issue1"
And I click on the edit button
Then I fill in "Notes" with "human Horn"
And I submit the form by the "Submit" button
And I should see "Successful update." within ".notice"
And I should see "human Horn" within ".work-package-details-activities-list"
@javascript
Scenario: User updates an issue with previewing the stuff before
When I go to the page of the issue "issue1"
And I click on the edit button
Then I fill in "Notes" with "human Horn"
When I follow "Preview"
Then I should see "human Horn" within "#preview"
And I submit the form by the "Submit" button
And I should see "Successful update." within ".notice"
And I should see "human Horn" within ".work-package-details-activities-list"
@javascript
Scenario: On an issue with children a user should not be able to change attributes which are overridden by children
Given the user "bob" has 1 issue with the following:
| subject | child1 |
When I go to the edit page of the work package "issue1"
Then there should not be a "Progress \(%\)" field
And there should be a disabled "Priority" field
And there should be a disabled "Start date" field
And there should be a disabled "Due date" field
And there should be a disabled "Estimated time" field

@ -34,7 +34,7 @@ Feature: Searching
And there are the following work packages in project "test-project": And there are the following work packages in project "test-project":
| subject | | subject |
| wp1 | | wp1 |
And I am admin And I am already admin
@javascript @selenium @javascript @selenium
Scenario: Searching stuff retains a project's scope Scenario: Searching stuff retains a project's scope
@ -43,5 +43,5 @@ Feature: Searching
And I search for "wp1" after having searched And I search for "wp1" after having searched
Then I should see "Overview" within "#main-menu" Then I should see "Overview" within "#main-menu"
And I click on "wp1" within "#search-results" And I click on "wp1" within "#search-results"
Then I should see "wp1" within "#work-package-subject" Then I should see "wp1" within ".work-packages--details--subject"
And I should be on the page of the work package "wp1" And I should be on the page of the work package "wp1"

@ -35,10 +35,16 @@ Then /^there should( not)? be an(?:y)? error message$/ do |no_message|
end end
end end
# This one aims at the rails flash based errors
Then /^I should see an error explanation stating "([^"]*)"$/ do |message| Then /^I should see an error explanation stating "([^"]*)"$/ do |message|
page.all(:css, '.errorExplanation li, .errorExplanation li *', text: message).should_not be_empty page.all(:css, '.errorExplanation li, .errorExplanation li *', text: message).should_not be_empty
end end
# This one aims at the angular js notifications which can be errors
Then /^I should see an error notification stating "([^"]*)"$/ do |message|
step "I should see \"#{message}\" within \".notification-box--errors li\""
end
Then /^there should( not)? be a flash (error|notice) message$/ do |no_message, kind_of_message| Then /^there should( not)? be a flash (error|notice) message$/ do |no_message, kind_of_message|
if no_message if no_message
should_not have_selector(".flash.#{kind_of_message}") should_not have_selector(".flash.#{kind_of_message}")

@ -48,11 +48,12 @@ Then(/^I should see the following changesets:$/) do |table|
# this will only work with one revision as we do not have proper markup # this will only work with one revision as we do not have proper markup
# to identify different changesets # to identify different changesets
within('.work-package-details-activities-list .revision-activity--revision-link') do within('.work-package-details-activities-list .revision-activity--revision-link') do
should have_content("committed revision #{row[:revision]}") expect(page).to have_content("committed revision #{row[:revision]}")
end end
end end
end end
Then(/^I should not be presented changesets$/) do Then(/^I should not be presented changesets$/) do
should_not have_selector('.work-package-details-activities-list .revision-activity--revision-link') expect(page)
.not_to have_selector('.work-package-details-activities-list .revision-activity--revision-link')
end end

@ -133,9 +133,7 @@ Then /^the work package should be shown with the following values:$/ do |table|
end end
if table.rows_hash['Subject'] if table.rows_hash['Subject']
expected_header = Regexp.new("#{table.rows_hash['Type']}\\s?#\\d+: #{table.rows_hash['Subject']}", Regexp::IGNORECASE) should have_css('.subject-header', text: table.rows_hash['Subject'])
should have_css('.subject-header', text: expected_header)
end end
if table.rows_hash['Description'] if table.rows_hash['Description']
@ -172,3 +170,30 @@ When /^I click the unwatch work package button$/ do
find('#unwatch-button').click find('#unwatch-button').click
end end
end end
When /^I fill in a comment with "(.+?)"$/ do |comment|
steps %{
And I click on "Click to add a comment"
Then I fill in "value" with "#{comment}" within ".work-packages--activity--add-comment"
}
end
When /^I preview the comment to be added and see "(.+?)"$/ do |comment|
steps %{
And I click on "Preview" within ".work-packages--activity--add-comment"
And I should see "#{comment}" within ".work-packages--activity--add-comment .-preview"
}
end
When /^I should see the comment "(.+?)"$/ do |comment|
steps %{
And I should see "#{comment}" within ".work-package-details-activities-list"
}
end
When /^I preview the "(.+?)" and see "(.+?)"$/ do |field_name, text|
steps %{
And I click on "Preview" within ".work-packages--details--#{field_name}"
And I should see "#{text}" within ".work-packages--details--#{field_name} .-preview"
}
end

@ -124,9 +124,9 @@ module NavigationHelpers
issue = WorkPackage.find_by(subject: $1) issue = WorkPackage.find_by(subject: $1)
"/work_packages/#{issue.id}" "/work_packages/#{issue.id}"
when /^the edit page (?:for|of) the issue "([^\"]+)"$/ when /^the edit page (?:for|of) the work package(?: called)? "([^\"]+)"$/
issue = WorkPackage.find_by(subject: $1) issue = WorkPackage.find_by(subject: $1)
"/issues/#{issue.id}/edit" "/work_packages/#{issue.id}/activity"
when /^the copy page (?:for|of) the work package "([^\"]+)"$/ when /^the copy page (?:for|of) the work package "([^\"]+)"$/
package = WorkPackage.find_by(subject: $1) package = WorkPackage.find_by(subject: $1)

@ -39,8 +39,8 @@ Feature: A work packages changesets are displayed on the work package show page
And the user "manager" is a "manager" And the user "manager" is a "manager"
And there are the following work packages in project "ecookbook": And there are the following work packages in project "ecookbook":
| subject | start_date | due_date | | subject | start_date | due_date |
| pe1 | 2013-01-01 | 2013-12-31 | | wp1 | 2013-01-01 | 2013-12-31 |
And the work package "pe1" has the following changesets: And the work package "wp1" has the following changesets:
| revision | committer | committed_on | comments | commit_date | | revision | committer | committed_on | comments | commit_date |
| 1 | manager | 2013-02-01 | blubs | 2013-02-01 | | 1 | manager | 2013-02-01 | blubs | 2013-02-01 |
And I am already logged in as "manager" And I am already logged in as "manager"
@ -50,7 +50,7 @@ Feature: A work packages changesets are displayed on the work package show page
Given the role "manager" may have the following rights: Given the role "manager" may have the following rights:
| view_work_packages | | view_work_packages |
| view_changesets | | view_changesets |
When I go to the page of the work package "pe1" When I go to the page of the work package "wp1"
Then I should see the following changesets: Then I should see the following changesets:
| revision | comments | | revision | comments |
| 1 | blubs | | 1 | blubs |
@ -59,5 +59,7 @@ Feature: A work packages changesets are displayed on the work package show page
Scenario: Going to the work package show page and not seeing the changesets because the user is not allowed to see them Scenario: Going to the work package show page and not seeing the changesets because the user is not allowed to see them
Given the role "manager" may have the following rights: Given the role "manager" may have the following rights:
| view_work_packages | | view_work_packages |
When I go to the page of the work package "pe1" When I go to the page of the work package "wp1"
# Safeguard to ensure the page is loaded
Then I should see "wp1" within ".work-packages--details--subject"
Then I should not be presented changesets Then I should not be presented changesets

@ -32,6 +32,8 @@ Feature: Fields editable on work package edit
| login | manager | | login | manager |
| firstname | the | | firstname | the |
| lastname | manager | | lastname | manager |
And the user "manager" has the following preferences
| warn_on_leaving_unsaved | false |
And there is a role "manager" And there is a role "manager"
And there is 1 project with the following: And there is 1 project with the following:
| identifier | ecookbook | | identifier | ecookbook |
@ -65,6 +67,8 @@ Feature: Fields editable on work package edit
| pe1 | pe1 description | 2013-01-01 | 2013-12-31 | 30 | Phase | manager | manager | prio1 | parentpe | 5 | version1 | | pe1 | pe1 description | 2013-01-01 | 2013-12-31 | 30 | Phase | manager | manager | prio1 | parentpe | 5 | version1 |
When I go to the edit page of the work package called "pe1" When I go to the edit page of the work package called "pe1"
And I click the edit work package button
And I click on "Show all"
Then I should see the following fields: Then I should see the following fields:
| Type | Phase | | Type | Phase |
@ -76,34 +80,14 @@ Feature: Fields editable on work package edit
| Version | version1 | | Version | version1 |
| Start date | 2013-01-01 | | Start date | 2013-01-01 |
| Due date | 2013-12-31 | | Due date | 2013-12-31 |
| Estimated time | 5.00 | | Estimated time | 5 |
| Progress (%) | 30 % | | Progress (%) | 30 |
| Notes | |
And the "Parent" field should contain the id of work package "parentpe"
When I click on "Relations"
Scenario: Going to the page and viewing timelog fields if this module is enabled Then I should see "parentpe" within ".relation[title='Parent']"
Given the role "manager" may have the following rights:
| edit_work_packages |
| view_work_packages |
| log_time |
And there are the following work packages in project "ecookbook":
| subject |
| pe1 |
And the project "ecookbook" uses the following modules:
| time_tracking |
And there is an activity "design"
When I go to the edit page of the work package called "pe1"
Then I should see the following fields:
| Spent time |
| Activity |
| Comment |
@javascript
Scenario: Going to the page and viewing custom field fields Scenario: Going to the page and viewing custom field fields
Given the role "manager" may have the following rights: Given the role "manager" may have the following rights:
| view_work_packages | | view_work_packages |
@ -127,6 +111,8 @@ Feature: Fields editable on work package edit
And the work package "pe1" has the custom field "cf1" set to "4" And the work package "pe1" has the custom field "cf1" set to "4"
When I go to the edit page of the work package called "pe1" When I go to the edit page of the work package called "pe1"
And I click the edit work package button
And I click on "Show all"
Then I should see the following fields: Then I should see the following fields:
| cf1 | 4 | | cf1 | 4 |

@ -33,6 +33,8 @@ Feature: Error messages are displayed
| login | manager | | login | manager |
| firstname | the | | firstname | the |
| lastname | manager | | lastname | manager |
And the user "manager" has the following preferences
| warn_on_leaving_unsaved | false |
And there is 1 project with the following: And there is 1 project with the following:
| identifier | ecookbook | | identifier | ecookbook |
| name | ecookbook | | name | ecookbook |
@ -40,7 +42,6 @@ Feature: Error messages are displayed
And the role "manager" may have the following rights: And the role "manager" may have the following rights:
| edit_work_packages | | edit_work_packages |
| view_work_packages | | view_work_packages |
| log_time |
And I am working in project "ecookbook" And I am working in project "ecookbook"
And the user "manager" is a "manager" And the user "manager" is a "manager"
And there are the following work packages in project "ecookbook": And there are the following work packages in project "ecookbook":
@ -52,8 +53,10 @@ Feature: Error messages are displayed
@javascript @javascript
Scenario: Inserting a too long subject results in an error beeing shown Scenario: Inserting a too long subject results in an error beeing shown
When I go to the edit page of the work package called "pe1" When I go to the edit page of the work package called "pe1"
And I click the edit work package button
And I click on "Show all"
And I fill in the following: And I fill in the following:
| Subject | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. mollit anim id est laborum. | | Subject | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. mollit anim id est laborum. |
And I submit the form by the "Submit" button And I submit the form by the "Save" button
Then I should see an error explanation stating "Subject is too long (maximum is 255 characters)" Then I should see an error notification stating "Subject is too long (maximum is 255 characters)"

@ -1,70 +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.
#++
Feature: Adding localized time log
Background:
Given the following languages are active:
| en |
| de |
And there is 1 user with:
| login | manager |
| firstname | the |
| lastname | manager |
| language | de |
And there is 1 project with the following:
| identifier | ecookbook |
| name | ecookbook |
And there is a role "manager"
And the role "manager" may have the following rights:
| edit_work_packages |
| view_work_packages |
| log_time |
And I am working in project "ecookbook"
And the project uses the following modules:
| time_tracking |
And the user "manager" is a "manager"
And there are the following status:
| name | default |
| status1 | true |
And there are the following work packages in project "ecookbook":
| subject | status_id |
| pe1 | 1 |
And there is an activity "design"
And I am already logged in as "manager"
@javascript
Scenario: Adding a localized time entry with a too long topic
Given I am on the edit page of the work package called "pe1"
When I fill in the following:
| Thema | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit |
| Aufgewendete Zeit | 2,5 |
| Aktivität | design |
And I submit the form by the "OK" button
Then I should be on the page of the work package "pe1"
And I should see 1 error message
And the "work_package_time_entry_hours" field should contain "2,5"

@ -1,64 +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.
#++
#
Feature: Logging time on work package update
Background:
Given there is 1 user with:
| login | manager |
| firstname | the |
| lastname | manager |
And there is 1 project with the following:
| identifier | ecookbook |
| name | ecookbook |
And there is a role "manager"
And the role "manager" may have the following rights:
| edit_work_packages |
| view_work_packages |
| log_time |
| view_time_entries |
And I am working in project "ecookbook"
And the user "manager" is a "manager"
And there are the following status:
| name | default |
| status1 | true |
And there are the following work packages in project "ecookbook":
| subject | status_id |
| pe1 | 1 |
And there is an activity "design"
And I am already logged in as "manager"
@javascript
Scenario: Logging time
When I go to the edit page of the work package called "pe1"
And I fill in the following:
| Spent time | 5 |
| Activity | design |
| Comment | Needed it |
And I submit the form by the "Submit" button
Then the work package should be shown with the following values:
| Spent time | 5.00 |

@ -30,6 +30,8 @@ Feature: Navigating to the work package edit page
Background: Background:
Given there is 1 user with: Given there is 1 user with:
| login | manager | | login | manager |
And the user "manager" has the following preferences
| warn_on_leaving_unsaved | false |
And there is a role "manager" And there is a role "manager"
And the role "manager" may have the following rights: And the role "manager" may have the following rights:
| edit_work_packages | | edit_work_packages |
@ -53,5 +55,4 @@ Feature: Navigating to the work package edit page
When I go to the page of the work package called "pe1" When I go to the page of the work package called "pe1"
# need to click on edit icon # need to click on edit icon
And I click the edit work package button And I click the edit work package button
# And I select "Update" from the action menu
Then I should be on the edit page of the work package called "pe1" Then I should be on the edit page of the work package called "pe1"

@ -60,11 +60,9 @@ Feature: Switching types of work packages
Scenario: Previewing changes on an existing work package Scenario: Previewing changes on an existing work package
Given there are the following work packages in project "project1": Given there are the following work packages in project "project1":
| subject | description | | subject | description |
| pe1 | pe1 description | | wp1 | wp1 description |
When I am on the edit page of the work package called "pe1" When I am on the edit page of the work package called "wp1"
And I click the edit work package button
And I fill in the following: And I fill in the following:
| Description | pe1 description changed | | Description | wp1 description changed |
| Notes | Update note | And I preview the "description" and see "wp1 description"
And I follow "Preview"
Then I should see "pe1 description changed" within "#preview"
Then I should see "Update note" within "#preview"

@ -46,6 +46,8 @@ Feature: Switching types of work packages
| login | bob | | login | bob |
| firstname | Bob | | firstname | Bob |
| lastname | Bobbit | | lastname | Bobbit |
And the user "bob" has the following preferences
| warn_on_leaving_unsaved | false |
And the user "bob" is a "member" in the project "project1" And the user "bob" is a "member" in the project "project1"
Given the user "bob" has 1 issue with the following: Given the user "bob" has 1 issue with the following:
| subject | wp1 | | subject | wp1 |
@ -56,6 +58,8 @@ Feature: Switching types of work packages
@javascript @javascript
Scenario: Switching type should keep the inserted value Scenario: Switching type should keep the inserted value
When I go to the edit page of the work package "wp1" When I go to the edit page of the work package "wp1"
And I click the edit work package button
And I click on "Show all"
And I fill in the following: And I fill in the following:
| Responsible | Bob Bobbit | | Responsible | Bob Bobbit |
And I select "Feature" from "Type" And I select "Feature" from "Type"
@ -77,6 +81,8 @@ Feature: Switching types of work packages
And the custom field "cfAll" is activated for type "Feature" And the custom field "cfAll" is activated for type "Feature"
When I go to the edit page of the work package "wp1" When I go to the edit page of the work package "wp1"
And I click the edit work package button
And I click on "Show all"
And I fill in the following: And I fill in the following:
| cfAll | 5 | | cfAll | 5 |
And I select "Feature" from "Type" And I select "Feature" from "Type"

@ -32,6 +32,8 @@ Feature: Updating work packages
| login | manager | | login | manager |
| firstname | the | | firstname | the |
| lastname | manager | | lastname | manager |
And the user "manager" has the following preferences
| warn_on_leaving_unsaved | false |
And there are the following types: And there are the following types:
| Name | Is milestone | | Name | Is milestone |
| Phase1 | false | | Phase1 | false |
@ -68,14 +70,14 @@ Feature: Updating work packages
And the type "Phase2" has the default workflow for the role "manager" And the type "Phase2" has the default workflow for the role "manager"
And there are the following work packages in project "ecookbook": And there are the following work packages in project "ecookbook":
| subject | type | status | fixed_version | | subject | type | status | fixed_version |
| pe1 | Phase1 | status1 | version1 | | wp1 | Phase1 | status1 | version1 |
| pe2 | | | |
And I am already logged in as "manager" And I am already logged in as "manager"
@javascript @wip @javascript
Scenario: Updating the work package and seeing the results on the show page Scenario: Updating the work package and seeing the results on the show page
# FIXME 16364 assignee is not shown on work package views (full and split screen) When I go to the edit page of the work package called "wp1"
When I go to the edit page of the work package called "pe1" And I click the edit work package button
And I click on "Show all"
And I fill in the following: And I fill in the following:
| Type | Phase2 | | Type | Phase2 |
# This is to be removed once the bug # This is to be removed once the bug
@ -88,13 +90,13 @@ Feature: Updating work packages
| Start date | 2013-03-04 | | Start date | 2013-03-04 |
| Due date | 2013-03-06 | | Due date | 2013-03-06 |
| Estimated time | 5.00 | | Estimated time | 5.00 |
| Progress (%) | 30 % | | Progress (%) | 30 |
| Priority | prio2 | | Priority | prio2 |
| Status | status2 | | Status | status2 |
| Subject | New subject | | Subject | New subject |
| Description | Desc2 | | Description | Desc2 |
And I fill in the id of work package "pe2" into "Parent" And I submit the form by the "Save" button
And I submit the form by the "Submit" button Then I should see "Successful update"
Then I should be on the page of the work package "New subject" Then I should be on the page of the work package "New subject"
And the work package should be shown with the following values: And the work package should be shown with the following values:
| Responsible | the manager | | Responsible | the manager |
@ -107,24 +109,47 @@ Feature: Updating work packages
| Subject | New subject | | Subject | New subject |
| Type | Phase2 | | Type | Phase2 |
| Description | Desc2 | | Description | Desc2 |
# And the work package "pe2" should be shown as the parent
@javascript @javascript
Scenario: Concurrent updates to work packages Scenario: Concurrent updates to work packages
When I go to the edit page of the work package called "pe1" When I go to the edit page of the work package called "wp1"
And I click the edit work package button
And I click on "Show all"
And I fill in the following: And I fill in the following:
| Start date | 03-04-2013 | | Start date | 03-04-2013 |
And the work_package "pe1" is updated with the following: And the work_package "wp1" is updated with the following:
| Start date | 04-04-2013 | | Start date | 04-04-2013 |
And I submit the form by the "Submit" button And I submit the form by the "Save" button
Then I should see "Information has been updated by at least one other user in the meantime." Then I should see an error notification stating "Couldn't update the resource because of conflicting modifications."
And I should see "The update(s) came from"
@javascript
Scenario: User adds a comment to a work package with previewing the stuff before
When I go to the page of the issue "wp1"
And I click on the edit button
And I fill in a comment with "human horn"
And I preview the comment to be added and see "human horn"
And I submit the form by the "Save" button
And I should see "The comment was successfully added."
And I should see the comment "human horn"
@javascript @javascript
Scenario: Adding a note Scenario: On a work package with children a user should not be able to change attributes which are overridden by children
When I go to the edit page of the work package called "pe1" And there are the following work packages in project "ecookbook":
And I fill in "Notes" with "Note message" | subject | type | status | fixed_version | priority | done_ratio | estimated_hours | start_date | due_date |
And I submit the form by the "Submit" button | child | Phase1 | status1 | version1 | prio2 | 50 | 5 | 2015-10-01 | 2015-10-30 |
Then I should be on the page of the work package "pe1" | parent | |   |   |   | 0 |   | | |
And I should see a journal with the following: Given the work package "parent" has the following children:
| Notes | Note message | | child |
When I go to the edit page of the work package "parent"
And I click the edit work package button
And I click on "Show all"
Then the work package should be shown with the following values:
| Priority | prio2 |
| Date | 10/01/2015 - 10/30/2015 |
| Estimated time | 5 |
| Progress (%) | 50 |
And there should not be a "Progress \(%\)" field
And there should not be a "Priority" field
And there should not be a "Start date" field
And there should not be a "End date" field
And there should not be a "Estimated time" field

@ -1,75 +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.
//++
/* globals Hyperagent */
require('hyperagent');
module.exports = function HALAPIResource($timeout, $q) {
'use strict';
var configure = function() {
Hyperagent.configure('ajax', function(settings) {
var deferred = $q.defer(),
resolve = settings.success,
reject = settings.error;
settings.success = deferred.resolve;
settings.reject = deferred.reject;
deferred.promise.then(function(response) {
$timeout(function() { resolve(response); });
}, function(reason) {
$timeout(function() { reject(reason); });
});
return jQuery.ajax(settings);
});
Hyperagent.configure('defer', $q.defer);
// keep this if you want null values to not be overwritten by
// Hyperagent.js miniscore
// this weird line replaces HA miniscore with normal underscore
// Freud would be happy with what ('_', _) reminds me of
Hyperagent.configure('_', _);
};
return {
setup: function(uri, params) {
if (!params) {
params = {};
}
configure();
var link = new Hyperagent.Resource(_.extend({
url: uri
}, params));
if (params.method) {
link.props.href = uri;
link.props.method = params.method;
}
return link;
}
};
};

@ -0,0 +1,64 @@
// -- 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.
// ++
/* globals Hyperagent */
require('hyperagent');
angular.module('openproject.api')
.run(run)
.factory('HALAPIResource', HALAPIResource);
function run($http, $q) {
Hyperagent.configure('ajax', function(settings) {
settings.transformResponse = function (data) { return data; };
return $http(settings).then(
function (response) { settings.success(response.data); },
settings.error
);
});
Hyperagent.configure('defer', $q.defer);
Hyperagent.configure('_', _);
}
run.$inject = ['$http', '$q'];
function HALAPIResource () {
return {
setup: function(uri, params) {
params = params || {};
var link = new Hyperagent.Resource(_.extend({ url: uri }, params));
if (params.method) {
link.props.href = uri;
link.props.method = params.method;
}
return link;
}
};
}

@ -26,27 +26,14 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
/*jshint expr: true*/
describe('HALAPIResource', function() { describe('HALAPIResource', function() {
var HALAPIResource, testPathHelper; var HALAPIResource;
beforeEach(module('openproject.api')); beforeEach(angular.mock.module('openproject.api'));
beforeEach(function() {
testPathHelper = {
apiV3: '/api/v3',
appBasePath: ''
};
module(function($provide) {
$provide.value('PathHelper', testPathHelper);
});
inject(function(_HALAPIResource_) { beforeEach(inject(function(_HALAPIResource_) {
HALAPIResource = _HALAPIResource_; HALAPIResource = _HALAPIResource_;
}); }));
});
describe('setup', function() { describe('setup', function() {
var apiResource, resourceFunction; var apiResource, resourceFunction;

@ -1,13 +1,13 @@
<div class="inplace-edit--read" ng-hide="fieldController.isEditing"> <div class="inplace-edit--read" ng-hide="fieldController.isEditing">
<accessible-by-keyboard <accessible-by-keyboard
ng-if="fieldController.isEditable()" ng-if="field.isEditable()"
class="inplace-editing--trigger-container" class="inplace-editing--trigger-container"
span-class="inplace-editing--container" span-class="inplace-editing--container"
link-class="inplace-editing--trigger-link" link-class="inplace-editing--trigger-link"
execute="displayPaneController.startEditing()"> execute="displayPaneController.startEditing()">
<span <span
class="inplace-edit--read-value" class="inplace-edit--read-value"
ng-class="{'-default': fieldController.isEmpty()}" ng-class="{'-default': field.isEmpty()}"
ng-include="templateUrl"> ng-include="templateUrl">
</span> </span>
<span class="inplace-edit--icon-wrapper"> <span class="inplace-edit--icon-wrapper">
@ -15,7 +15,7 @@
</icon-wrapper> </icon-wrapper>
</span> </span>
</accessible-by-keyboard> </accessible-by-keyboard>
<span ng-if="!fieldController.isEditable()"> <span ng-if="!field.isEditable()">
<span <span
class="inplace-edit--read-value" class="inplace-edit--read-value"
ng-include="templateUrl"> ng-include="templateUrl">

@ -26,88 +26,42 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function( angular
WorkPackageFieldService, .module('openproject.inplace-edit')
EditableFieldsState, .directive('inplaceEditorDisplayPane', inplaceEditorDisplayPane);
$timeout,
HookService, function inplaceEditorDisplayPane(EditableFieldsState, $timeout) {
I18n) {
return { return {
replace: true, replace: true,
transclude: true, transclude: true,
scope: {},
require: '^workPackageField', require: '^workPackageField',
templateUrl: '/templates/work_packages/inplace_editor/display_pane.html', templateUrl: '/components/inplace-edit/directives/display-pane/display-pane.directive.html',
controller: function($scope) { controller: InplaceEditorDisplayPaneController,
this.placeholder = WorkPackageFieldService.defaultPlaceholder;
this.startEditing = function() {
var fieldController = $scope.fieldController;
fieldController.isEditing = true;
};
this.isReadValueEmpty = function() {
return WorkPackageFieldService.isEmpty(
EditableFieldsState.workPackage,
$scope.fieldController.field
);
};
this.getReadValue = function() {
return WorkPackageFieldService.format(
EditableFieldsState.workPackage,
$scope.fieldController.field
);
};
// for dynamic type that is set by plugins
// refactor to a service method the whole extraction
this.getDynamicDirectiveName = function() {
return HookService.call('workPackageOverviewAttributes', {
type: EditableFieldsState.workPackage.schema.props[$scope.fieldController.field].type,
field: $scope.fieldController.field,
workPackage: EditableFieldsState.workPackage
})[0];
};
// expose work package to the dynamic directive
this.getWorkPackage = function() {
return EditableFieldsState.workPackage;
};
},
controllerAs: 'displayPaneController', controllerAs: 'displayPaneController',
link: function(scope, element, attrs, fieldController) { link: function(scope, element, attrs, fieldController) {
var field = scope.field;
scope.fieldController = fieldController; scope.fieldController = fieldController;
scope.displayPaneController.field = scope.fieldController.field;
scope.editableFieldsState = EditableFieldsState; scope.editableFieldsState = EditableFieldsState;
scope.$watchCollection('editableFieldsState.workPackage.form', function() { scope.$watchCollection('editableFieldsState.workPackage.form', function() {
var strategy = WorkPackageFieldService.getInplaceDisplayStrategy( var strategy = field.getInplaceDisplayStrategy();
EditableFieldsState.workPackage,
fieldController.field
);
if (strategy !== scope.displayStrategy) { if (strategy !== scope.displayStrategy) {
scope.displayStrategy = strategy; scope.displayStrategy = strategy;
scope.templateUrl = '/templates/components/inplace_editor/display/' + strategy +'.html'; scope.templateUrl = '/templates/inplace-edit/display/fields/' + strategy +'.html';
} }
}); });
// TODO: extract this when more placeholders come
if (fieldController.field === 'description') {
scope.displayPaneController.placeholder = I18n.t('js.label_click_to_enter_description');
}
scope.$watch('editableFieldsState.errors', function(errors) { scope.$watch('editableFieldsState.errors', function(errors) {
if (errors) { if (errors && errors[field.name] && field.isEditable()) {
if (errors[scope.fieldController.field]) {
scope.displayPaneController.startEditing(); scope.displayPaneController.startEditing();
} }
}
}, true); }, true);
scope.$watch('fieldController.isEditing', function(isEditing, oldIsEditing) { scope.$watch('fieldController.isEditing', function(isEditing, oldIsEditing) {
if (!isEditing) { if (!isEditing && !fieldController.lockFocus) {
$timeout(function() { $timeout(function() {
if (oldIsEditing) { if (oldIsEditing) {
// check old value to not trigger focus on the first time // check old value to not trigger focus on the first time
@ -118,7 +72,36 @@ module.exports = function(
}); });
}); });
} }
fieldController.lockFocus = false;
}); });
} }
}; };
}
inplaceEditorDisplayPane.$inject = ['EditableFieldsState', '$timeout'];
function InplaceEditorDisplayPaneController($scope, HookService) {
var field = $scope.field;
this.placeholder = field.placeholder;
this.startEditing = function() {
if (!field.isEditable()) {
throw 'Trying to edit the non editable field "' + field.name + '"';
}
var fieldController = $scope.fieldController;
fieldController.isEditing = true;
};
// for dynamic type that is set by plugins
// refactor to a service method the whole extraction
this.getDynamicDirectiveName = function() {
return HookService.call('workPackageOverviewAttributes', {
type: field.resource.schema.props[field.name].type,
field: field.name,
workPackage: field.resource
})[0];
}; };
}
InplaceEditorDisplayPaneController.$inject = ['$scope', 'HookService'];

@ -1,6 +1,6 @@
<div class="inplace-edit--write edit-strategy-{{ strategy }}" ng-show="fieldController.isEditing"> <div class="inplace-edit--write edit-strategy-{{ strategy }}" ng-show="fieldController.isEditing">
<form class="inplace-edit--form" ng-if="fieldController.isEditing" name="editPaneController.editForm" ng-submit="editPaneController.submit()" novalidate> <form class="inplace-edit--form" ng-if="fieldController.isEditing" name="editPaneController.editForm" ng-submit="editPaneController.submit()" novalidate>
<div class="inplace-edit--write-value" ng-include="templateUrl" ng-click="editPaneController.markActive()" tabindex="-1" ng-attr-role="{{ strategy == 'text' || 'wiki_textarea' ? 'textbox' : 'button' }}"> <div class="inplace-edit--write-value" ng-include="templateUrl" tabindex="-1" ng-attr-role="{{ strategy == 'text' || 'wiki_textarea' ? 'textbox' : 'button' }}">
</div> </div>
<div class="inplace-edit--dashboard"> <div class="inplace-edit--dashboard">
<div class="inplace-edit--controls" ng-hide="fieldController.state.isBusy || !editPaneController.isActive()"> <div class="inplace-edit--controls" ng-hide="fieldController.state.isBusy || !editPaneController.isActive()">

@ -0,0 +1,286 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorEditPane', inplaceEditorEditPane);
function inplaceEditorEditPane(EditableFieldsState, FocusHelper, $timeout, $q) {
return {
transclude: true,
replace: true,
require: '^workPackageField',
templateUrl: '/components/inplace-edit/directives/edit-pane/edit-pane.directive.html',
controllerAs: 'editPaneController',
controller: InplaceEditorEditPaneController,
link: function(scope, element, attrs, fieldController) {
var field = scope.field;
scope.fieldController = fieldController;
scope.editableFieldsState = EditableFieldsState;
scope.editPaneController.isRequired = function() {
return field.isRequired();
};
scope.$on('form.updateRequired', function() {
var submit = $q.defer();
scope.editPaneController.updateWorkPackageForm(submit);
});
scope.$watchCollection('editableFieldsState.workPackage.form', function(form) {
var strategy = field.getInplaceEditStrategy();
if (field.name === 'date' && strategy === 'date') {
form.pendingChanges = EditableFieldsState.getPendingFormChanges();
form.pendingChanges['startDate'] =
form.pendingChanges['dueDate'] =
field.value ? field.value['dueDate'] : null;
}
if (strategy !== scope.strategy) {
scope.strategy = strategy;
scope.templateUrl = '/templates/inplace-edit/edit/fields/' +
scope.strategy + '.html';
}
});
scope.focusInput = function() {
$timeout(function() {
var inputElement = element.find('.focus-input');
FocusHelper.focus(inputElement);
inputElement.triggerHandler('keyup');
scope.editPaneController.markActive();
inputElement.off('focus.inplace').on('focus.inplace', function() {
// ♥♥♥ angular ♥♥♥
scope.$apply(function() {
scope.editPaneController.markActive();
});
});
});
};
if (!EditableFieldsState.forcedEditState) {
element.bind('keydown keypress', function(e) {
if (e.keyCode === 27 && !EditableFieldsState.editAll.state) {
scope.$apply(function() {
scope.editPaneController.discardEditing();
});
}
});
}
scope.$watch('field.value', function(value) {
if (scope.fieldController.isEditing) {
var pendingChanges = EditableFieldsState.getPendingFormChanges();
pendingChanges[field.name] = value;
scope.editPaneController.markActive();
}
}, true);
scope.$on('workPackageRefreshed', function() {
scope.editPaneController.discardEditing();
});
scope.$watch('fieldController.isEditing', function(isEditing) {
var efs = EditableFieldsState;
if (isEditing && !efs.editAll.state && !efs.forcedEditState) {
scope.focusInput();
} else if (efs.editAll.state && efs.editAll.isFocusField(field.name)) {
$timeout(function () {
var focusElement = element.find('.focus-input');
focusElement.length && focusElement.focus()[0].select();
});
}
});
}
};
}
inplaceEditorEditPane.$inject = ['EditableFieldsState', 'FocusHelper', '$timeout', '$q'];
function InplaceEditorEditPaneController($scope, $element, $location, $timeout, $q, $rootScope,
WorkPackageService, EditableFieldsState, ApiHelper, NotificationsService) {
var showErrors = function() {
var errors = EditableFieldsState.errors;
if (_.isEmpty(_.keys(errors))) {
return;
}
var errorMessages = _.flatten(_.map(errors), true);
NotificationsService.addError(I18n.t('js.label_validation_error'), errorMessages);
};
var vm = this;
var field = $scope.field;
// go full retard
var uploadPendingAttachments = function(wp) {
$rootScope.$broadcast('uploadPendingAttachments', wp);
};
// Propagate submission to all active fields
// not contained in the workPackage.form (e.g., comment)
this.submit = function() {
EditableFieldsState.save(function() {
// Clears the location hash, as we're now
// scrolling to somewhere else
$location.hash(null);
$timeout(function() {
$element[0].scrollIntoView(false);
});
});
};
this.handleFailure = function(e, submit) {
setFailure(e);
submit.reject(e);
};
this.updateWorkPackageForm = function(submit) {
WorkPackageService.loadWorkPackageForm(EditableFieldsState.workPackage).then(
function(form) {
field.resource.form = form;
EditableFieldsState.workPackage.form = form;
if (_.isEmpty(form.embedded.validationErrors.props)) {
submit.resolve();
} else {
afterError();
submit.reject();
EditableFieldsState.errors = {};
_.forEach(form.embedded.validationErrors.props, function(error, field) {
if(field === 'startDate' || field === 'dueDate') {
EditableFieldsState.errors['date'] = error.message;
} else {
EditableFieldsState.errors[field] = error.message;
}
});
showErrors();
}
}).catch(function(e) {
vm.handleFailure(e, submit);
});
return submit.promise;
};
this.submitField = function() {
var submit = $q.defer();
var fieldController = $scope.fieldController;
var pendingFormChanges = EditableFieldsState.getPendingFormChanges();
var detectedViolations = [];
pendingFormChanges[field.name] = field.value;
if (vm.editForm.$invalid) {
var acknowledgedValidationErrors = Object.keys(vm.editForm.$error);
acknowledgedValidationErrors.forEach(function(error) {
if (vm.editForm.$error[error]) {
detectedViolations.push(I18n.t('js.inplace.errors.' + error, {
field: field.getLabel()
}));
}
});
submit.reject();
}
if (detectedViolations.length) {
EditableFieldsState.errors = EditableFieldsState.errors || {};
EditableFieldsState.errors[field.name] = detectedViolations.join(' ');
showErrors();
submit.reject();
} else {
fieldController.state.isBusy = true;
vm.updateWorkPackageForm(submit).then(function() {
var result = WorkPackageService.updateWorkPackage(
EditableFieldsState.workPackage
);
result.then(angular.bind(this, function(updatedWorkPackage) {
submit.resolve();
field.resource = _.extend(field.resource, updatedWorkPackage);
$scope.$emit('workPackageUpdatedInEditor', updatedWorkPackage);
$scope.$on('workPackageRefreshed', function() {
fieldController.state.isBusy = false;
fieldController.isEditing = false;
});
uploadPendingAttachments(updatedWorkPackage);
})).catch(function(e) {
vm.handleFailure(e, submit);
});
});
}
return submit.promise;
};
this.discardEditing = function() {
$scope.fieldController.isEditing = false;
delete EditableFieldsState.submissionPromises['work_package'];
delete EditableFieldsState.getPendingFormChanges()[field.name];
if (
EditableFieldsState.errors &&
EditableFieldsState.errors.hasOwnProperty(field.name)
) {
delete EditableFieldsState.errors[field.name];
}
};
this.isActive = function() {
return EditableFieldsState.isActiveField(field.name);
};
this.markActive = function() {
EditableFieldsState.submissionPromises['work_package'] = {
field: field.name,
thePromise: this.submitField,
prepend: true
};
EditableFieldsState.currentField = field.name;
};
function afterError() {
$scope.fieldController.state.isBusy = false;
$scope.focusInput();
}
function setFailure(e) {
afterError();
EditableFieldsState.errors = {
'_common': ApiHelper.getErrorMessages(e)
};
showErrors();
}
$scope.$watch('editableFieldsState.editAll.state', function(state) {
$scope.fieldController.isEditing = state;
$scope.fieldController.lockFocus = true;
});
}
InplaceEditorEditPaneController.$inject = ['$scope', '$element', '$location', '$timeout', '$q',
'$rootScope', 'WorkPackageService', 'EditableFieldsState', 'ApiHelper', 'NotificationsService'];

@ -1,13 +1,13 @@
<div class="spent-time-wrapper"> <div class="spent-time-wrapper">
<span ng-if="fieldController.isEmpty()">{{ displayPaneController.placeholder }}</span> <span ng-if="field.isEmpty()">{{ field.placeholder }}</span>
<span ng-if="!fieldController.isEmpty()"> <span ng-if="!field.isEmpty()">
<span ng-if="customEditorController.isLinkViewable()"> <span ng-if="customEditorController.isLinkViewable()">
<a href="{{ customEditorController.getPath() }}"> <a href="{{ customEditorController.getPath() }}">
{{ displayPaneController.getReadValue() }} {{ field.text }}
</a> </a>
</span> </span>
<span ng-if="!customEditorController.isLinkViewable()"> <span ng-if="!customEditorController.isLinkViewable()">
{{ displayPaneController.getReadValue() }} {{ field.text }}
</span> </span>
</span> </span>
</div> </div>

@ -26,24 +26,32 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function(EditableFieldsState, PathHelper, VersionService, $timeout) { angular
.module('openproject.inplace-edit')
.directive('inplaceDisplaySpentTime', inplaceDisplaySpentTime);
function inplaceDisplaySpentTime() {
return { return {
restrict: 'E', restrict: 'E',
transclude: true, transclude: true,
replace: true, replace: true,
scope: {}, templateUrl: '/components/inplace-edit/directives/field-display/display-spent-time/' +
require: '^inplaceEditorDisplayPane', 'display-spent-time.directive.html',
templateUrl: '/templates/work_packages/inplace_editor/custom/display/version.html',
controller: function($scope) { controller: InplaceDisplaySpentTimeController,
this.pathHelper = PathHelper; controllerAs: 'customEditorController'
this.isVersionLinkViewable = function() { };
var version = $scope.displayPaneController.getReadValue();
return version.links.definingProject && version.links.definingProject.href;
}
},
controllerAs: 'customEditorController',
link: function(scope, element, attrs, displayPaneController) {
scope.displayPaneController = displayPaneController;
} }
function InplaceDisplaySpentTimeController($scope) {
var field = $scope.field;
this.isLinkViewable = function() {
return field.resource.links.timeEntries;
}; };
this.getPath = function() {
return field.resource.links.timeEntries.href;
}; };
}
InplaceDisplaySpentTimeController.$inject = ['$scope'];

@ -0,0 +1,28 @@
<span class="user-avatar--container">
<img class="user-avatar--avatar"
ng-if="user && user.avatar"
ng-src="{{user.avatar}}"
alt="Avatar"
title="{{user.name}}" />
<span class="user-avatar--user-with-role">
<span class="user-avatar--user">
<a ng-if="!user.isGroup"
ng-href="{{user.href}}"
class="user-field-user-link">
{{user.name}}
</a>
<span ng-if="user.isGroup"
class="user-field-user-link">
{{user.name}}
</span>
</span>
<span class="user-avatar--user"
ng-if="!user">
{{ field.placeholder }}
</span>
<span class="user-avatar--role"
ng-if="user.role">
{{user.role}}
</span>
</span>
</span>

@ -0,0 +1,93 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
angular
.module('openproject.inplace-edit')
.directive('inplaceDisplayUser', inplaceDisplayUser);
function inplaceDisplayUser() {
return {
restrict: 'E',
transclude: true,
replace: true,
require: '^inplaceEditorDisplayPane',
templateUrl: '/components/inplace-edit/directives/field-display/display-user/' +
'display-user.directive.html',
controller: InplaceDisplayUserController,
controllerAs: 'customEditorController',
link: function(scope, element, attrs, inplaceEditorDisplayPane) {
scope.inplaceEditorDisplayPane = inplaceEditorDisplayPane;
scope.$watch('field.text', function(value) {
scope.customEditorController.initializeUserWith(value);
});
}
};
}
function InplaceDisplayUserController($scope, PathHelper) {
var getUserName = function(user) {
if (user && user.props) {
return user.props.name;
}
};
var getIsGroup = function(user) {
return user.props.subtype === 'Group';
};
var getHref = function(user) {
var id = user.props.id;
return PathHelper.staticUserPath(id);
};
var getAvatar = function(user) {
return user.props.avatar;
};
var getRole = function(userData) {
return userData.props.role;
};
this.initializeUserWith = function(userData) {
$scope.user = userData;
if (userData) {
$scope.user.name = getUserName(userData);
$scope.user.isGroup = getIsGroup(userData);
$scope.user.href = getHref(userData);
$scope.user.avatar = getAvatar(userData);
$scope.user.role = getRole(userData);
}
};
}
InplaceDisplayUserController.$inject = ['$scope', 'PathHelper'];

@ -0,0 +1,13 @@
<div class="version-wrapper">
<span ng-if="!field.text">
{{field.placeholder}}
</span>
<span ng-if="field.text && customEditorController.isVersionLinkViewable()">
<a ng-href="{{ customEditorController.versionLink }}">
{{field.text.props.name}}
</a>
</span>
<span ng-if="field.text && !customEditorController.isVersionLinkViewable()">
{{field.text.props.name}}
</span>
</div>

@ -26,26 +26,31 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function() { angular
.module('openproject.inplace-edit')
.directive('inplaceDisplayVersion', inplaceDisplayVersion);
function inplaceDisplayVersion() {
return { return {
restrict: 'E',
transclude: true, transclude: true,
replace: true, replace: true,
scope: false, templateUrl: '/components/inplace-edit/directives/field-display/display-version/' +
templateUrl: '/templates/work_packages/inplace_editor/main_pane.html', 'display-version.directive.html',
controller: function($scope, $timeout) {
// controller is invoked before linker controller: InplaceDisplayVersionController,
$timeout(function() { controllerAs: 'customEditorController'
var fieldController = $scope.fieldController;
this.saveTitle = I18n.t(
'js.inplace.button_save',
{ attribute: fieldController.field }
);
this.cancelTitle = I18n.t(
'js.inplace.button_cancel',
{ attribute: fieldController.field }
);
});
},
controllerAs: 'mainPaneController',
};
}; };
}
function InplaceDisplayVersionController($scope, PathHelper) {
var field = $scope.field;
this.versionLink = field.text && PathHelper.staticVersionPath(field.text.props.id);
this.isVersionLinkViewable = function() {
var version = field.text;
return version.links.definingProject && version.links.definingProject.href;
}
}
InplaceDisplayVersionController.$inject = ['$scope', 'PathHelper'];

@ -1,12 +1,24 @@
<div class="inplace-edit--date-range"> <div class="inplace-edit--date-range">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}-start">
{{::startDateLabel}}
</label>
<input type="text" <input type="text"
class="inplace-edit--date-range-start-date" class="inplace-edit--date-range-start-date"
id="inplace-edit--write-value--{{::field.name}}-start"
ng-change="onStartEdit()" ng-change="onStartEdit()"
ng-model="startDate" ng-class="{'inplace-edit--highlight': startDateIsChanged}" /> ng-model="startDate" ng-class="{'inplace-edit--highlight': startDateIsChanged}" />
<div class="inplace-edit--date-range-start-date-picker"></div> <div class="inplace-edit--date-range-start-date-picker"></div>
<span class="delimeter">-</span> <span class="delimeter">-</span>
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}-end">
{{::endDateLabel}}
</label>
<input type="text" <input type="text"
class="inplace-edit--date-range-end-date" class="inplace-edit--date-range-end-date"
id="inplace-edit--write-value--{{::field.name}}-end"
ng-change="onEndEdit()" ng-change="onEndEdit()"
ng-model="endDate" ng-class="{'inplace-edit--highlight': endDateIsChanged}" /> ng-model="endDate" ng-class="{'inplace-edit--highlight': endDateIsChanged}" />
<div class="inplace-edit--date-range-end-date-picker"></div> <div class="inplace-edit--date-range-end-date-picker"></div>

@ -26,24 +26,27 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function(TimezoneService, ConfigurationService, angular
I18n, $timeout, WorkPackageFieldService, .module('openproject.inplace-edit')
.directive('inplaceEditorDateRange', inplaceEditorDateRange);
function inplaceEditorDateRange(TimezoneService, I18n, $timeout, WorkPackageFieldService,
EditableFieldsState, Datepicker) { EditableFieldsState, Datepicker) {
return { return {
restrict: 'E', restrict: 'E',
transclude: true, transclude: true,
replace: true, replace: true,
scope: {}, templateUrl: '/components/inplace-edit/directives/field-edit/edit-date-range/' +
require: '^workPackageField', 'edit-date-range.directive.html',
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/daterange.html',
controller: function() { controller: function() {},
},
controllerAs: 'customEditorController', controllerAs: 'customEditorController',
link: function(scope, element, attrs, fieldController) {
link: function(scope, element) {
var field = scope.field;
var customDateFormat = 'YYYY-MM-DD'; var customDateFormat = 'YYYY-MM-DD';
function getTitle(labelName) { function getTitle(labelName) {
return I18n.t('js.inplace.button_edit', { return I18n.t('js.inplace.button_edit', {
attribute: WorkPackageFieldService.getLabel( attribute: WorkPackageFieldService.getLabel(
@ -53,8 +56,13 @@ module.exports = function(TimezoneService, ConfigurationService,
}); });
} }
scope.startDate = fieldController.writeValue.startDate; scope.startDate = field.value.startDate;
scope.endDate = fieldController.writeValue.dueDate; scope.endDate = field.value.dueDate;
// TODO: make this work package agnostic
scope.startDateLabel = I18n.t('js.work_packages.properties.startDate');
scope.endDateLabel = I18n.t('js.work_packages.properties.dueDate');
var form = element.parents('.inplace-edit--form'), var form = element.parents('.inplace-edit--form'),
inputStart = element.find('.inplace-edit--date-range-start-date'), inputStart = element.find('.inplace-edit--date-range-start-date'),
inputEnd = element.find('.inplace-edit--date-range-end-date'), inputEnd = element.find('.inplace-edit--date-range-end-date'),
@ -78,10 +86,10 @@ module.exports = function(TimezoneService, ConfigurationService,
startDatepicker = new Datepicker(divStart, inputStart, scope.startDate); startDatepicker = new Datepicker(divStart, inputStart, scope.startDate);
endDatepicker = new Datepicker(divEnd, inputEnd, scope.endDate); endDatepicker = new Datepicker(divEnd, inputEnd, scope.endDate);
startDatepicker.onChange = function(date) { startDatepicker.onChange = function(date) {
scope.startDate = fieldController.writeValue.startDate = date; scope.startDate = field.value.startDate = date;
if (startDatepicker.prevDate.isAfter(endDatepicker.prevDate)) { if (startDatepicker.prevDate.isAfter(endDatepicker.prevDate)) {
scope.startDateIsChanged = true; scope.startDateIsChanged = true;
scope.endDate = fieldController.writeValue.dueDate = scope.startDate; scope.endDate = field.value.dueDate = scope.startDate;
endDatepicker.setDate(scope.endDate); endDatepicker.setDate(scope.endDate);
} }
}; };
@ -90,10 +98,10 @@ module.exports = function(TimezoneService, ConfigurationService,
startDatepicker.onEdit(); startDatepicker.onEdit();
}; };
endDatepicker.onChange = function(date) { endDatepicker.onChange = function(date) {
scope.endDate = fieldController.writeValue.dueDate = date; scope.endDate = field.value.dueDate = date;
if (endDatepicker.prevDate.isBefore(startDatepicker.prevDate)) { if (endDatepicker.prevDate.isBefore(startDatepicker.prevDate)) {
scope.endDateIsChanged = true; scope.endDateIsChanged = true;
scope.startDate = fieldController.writeValue.startDate = scope.endDate; scope.startDate = field.value.startDate = scope.endDate;
startDatepicker.setDate(scope.startDate); startDatepicker.setDate(scope.startDate);
} }
}; };
@ -103,11 +111,13 @@ module.exports = function(TimezoneService, ConfigurationService,
}; };
startDatepicker.onDone = endDatepicker.onDone = function() { startDatepicker.onDone = endDatepicker.onDone = function() {
$timeout(function() {
form.scope().editPaneController.discardEditing(); form.scope().editPaneController.discardEditing();
});
}; };
$timeout(function() { $timeout(function() {
startDatepicker.focus(); EditableFieldsState.editAll.state || startDatepicker.focus();
}); });
startDatepicker.textbox.on('click focusin', function() { startDatepicker.textbox.on('click focusin', function() {
@ -145,4 +155,6 @@ module.exports = function(TimezoneService, ConfigurationService,
}); });
} }
}; };
}; }
inplaceEditorDateRange.$inject = ['TimezoneService', 'I18n', '$timeout', 'WorkPackageFieldService',
'EditableFieldsState', 'Datepicker'];

@ -0,0 +1,15 @@
<div class="inplace-edit--date">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<input ng-model="field.value"
ng-change="onEdit()"
ng-click="showDatepicker()"
title="{{ fieldController.editTitle }}"
class="inplace-edit--date"
id="inplace-edit--write-value--{{::field.name}}"
type="text" />
<div class="inplace-edit--date-picker"></div>
</div>

@ -26,26 +26,31 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function(WorkPackageFieldService, EditableFieldsState, angular
TimezoneService, ConfigurationService, I18n, .module('openproject.inplace-edit')
$timeout, Datepicker) { .directive('inplaceEditorDate', inplaceEditorDate);
function inplaceEditorDate(EditableFieldsState, TimezoneService, $timeout, Datepicker) {
var parseISODate = TimezoneService.parseISODate, var parseISODate = TimezoneService.parseISODate,
customDateFormat = 'YYYY-MM-DD', customDateFormat = 'YYYY-MM-DD',
customFormattedDate = function(date) { customFormattedDate = function(date) {
return parseISODate(date).format(customDateFormat); return parseISODate(date).format(customDateFormat);
}; };
return { return {
restrict: 'E', restrict: 'E',
transclude: true, transclude: true,
replace: true, replace: true,
scope: {},
require: '^workPackageField', require: '^workPackageField',
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/date.html', templateUrl: '/components/inplace-edit/directives/field-edit/edit-date/' +
controller: function() { 'edit-date.directive.html',
},
controller: function() {},
controllerAs: 'customEditorController', controllerAs: 'customEditorController',
link: function(scope, element, attrs, fieldController) {
scope.fieldController = fieldController; link: function(scope, element) {
var field = scope.field;
var form = element.parents('.inplace-edit--form'), var form = element.parents('.inplace-edit--form'),
input = element.find('.inplace-edit--date'), input = element.find('.inplace-edit--date'),
datepickerContainer = element.find('.inplace-edit--date-picker'), datepickerContainer = element.find('.inplace-edit--date-picker'),
@ -55,13 +60,11 @@ module.exports = function(WorkPackageFieldService, EditableFieldsState,
form.scope().editPaneController.submit(); form.scope().editPaneController.submit();
}; };
if(scope.fieldController.writeValue) { field.value = field.value && customFormattedDate(field.value);
scope.fieldController.writeValue = customFormattedDate(scope.fieldController.writeValue);
}
datepicker = new Datepicker(datepickerContainer, input, scope.fieldController.writeValue); datepicker = new Datepicker(datepickerContainer, input, field.value);
datepicker.onChange = function(date) { datepicker.onChange = function(date) {
scope.fieldController.writeValue = date; field.value = date;
}; };
scope.onEdit = function() { scope.onEdit = function() {
datepicker.onEdit(); datepicker.onEdit();
@ -80,7 +83,7 @@ module.exports = function(WorkPackageFieldService, EditableFieldsState,
}; };
$timeout(function() { $timeout(function() {
datepicker.focus(); EditableFieldsState.editAll.state || datepicker.focus();
}); });
angular.element('.work-packages--details-content').on('click', function(e) { angular.element('.work-packages--details-content').on('click', function(e) {
@ -93,4 +96,5 @@ module.exports = function(WorkPackageFieldService, EditableFieldsState,
}); });
} }
}; };
}; }
inplaceEditorDate.$inject = ['EditableFieldsState', 'TimezoneService', '$timeout', 'Datepicker'];

@ -0,0 +1,15 @@
<div class="dropdown-wrapper">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<select
ng-disabled="fieldController.state.isBusy"
ng-model="field.value.props"
title="{{ fieldController.editTitle }}"
class="inplace-edit-select"
id="inplace-edit--write-value--{{::field.name}}"
ng-options="option as option.name for option in customEditorController.allowedValues track by option.hrefTracker">
</select>
</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.
// ++
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorDropDown', inplaceEditorDropDown);
function inplaceEditorDropDown(EditableFieldsState, FocusHelper) {
return {
restrict: 'E',
transclude: true,
replace: true,
require: '^workPackageField',
templateUrl: '/components/inplace-edit/directives/field-edit/edit-drop-down/' +
'edit-drop-down.directive.html',
controller: InplaceEditorDropDownController,
controllerAs: 'customEditorController',
link: function(scope, element, attrs, fieldController) {
var field = scope.field;
fieldController.state.isBusy = true;
scope.emptyField = !scope.field.isRequired();
scope.customEditorController.updateAllowedValues(field.name).then(function() {
fieldController.state.isBusy = false;
if (!EditableFieldsState.forcedEditState) {
EditableFieldsState.editAll.state || FocusHelper.focusUiSelect(element);
}
});
}
};
}
inplaceEditorDropDown.$inject = ['EditableFieldsState', 'FocusHelper'];
function InplaceEditorDropDownController($q, $scope, I18n, WorkPackageFieldConfigurationService) {
this.allowedValues = [];
this.updateAllowedValues = function(field) {
var customEditorController = this;
return $q(function(resolve) {
$scope.field.getAllowedValues()
.then(function(values) {
var sorting = WorkPackageFieldConfigurationService
.getDropdownSortingStrategy(field);
if (sorting !== null) {
values = _.sortBy(values, sorting);
}
if (!$scope.field.isRequired()) {
values = addEmptyOption(values);
}
addHrefTracker(values);
customEditorController.allowedValues = values;
resolve();
});
});
};
var addEmptyOption = function(values) {
var emptyOption = { props: { href: null,
name: $scope.field.placeholder } };
if (!$scope.field.isRequired()) {
var arrayWithEmptyOption = [emptyOption.props];
values = arrayWithEmptyOption.concat(values);
if ($scope.field.value === null) {
$scope.field.value = emptyOption;
}
}
return values;
};
// We have to maintain a separate property just to track the object by
// in the template. This is due to angular aparently not being able to
// track correclty with a field having null as it's value. It does work for
// 'null' (String) however.
var addHrefTracker = function(values) {
_.forEach(values, function(value) {
value.hrefTracker = String(value.href);
});
$scope.field.value.props.hrefTracker = String($scope.field.value.props.href);
};
}
InplaceEditorDropDownController.$inject = ['$q', '$scope', 'I18n',
'WorkPackageFieldConfigurationService'];

@ -0,0 +1,16 @@
<div class="edit-duration-wrapper">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<input class="focus-input inplace-edit--text-field"
id="inplace-edit--write-value--{{::field.name}}"
name="value"
type="number"
step="0.5"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="numberValue" />
</div>

@ -26,38 +26,47 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function() { angular
.module('openproject.inplace-edit')
.directive('inplaceEditorDuration', inplaceEditorDuration);
function inplaceEditorDuration() {
return { return {
restrict: 'E', restrict: 'E',
transclude: true, transclude: true,
replace: true, replace: true,
scope: {}, templateUrl: '/components/inplace-edit/directives/field-edit/edit-duration/' +
require: '^workPackageField', 'edit-duration.directive.html',
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/duration.html',
controllerAs: 'customEditorController', controllerAs: 'customEditorController',
controller: function() {}, controller: function() {},
link: function(scope, element, attrs, fieldController) {
scope.fieldController = fieldController; link: function(scope) {
if (fieldController.writeValue === null) { var field = scope.field;
scope.customEditorController.writeValue = null; scope.numberValue = 0;
} else {
scope.customEditorController.writeValue = Number( if (field.value) {
moment scope.numberValue = Number(moment.duration(field.value).asHours().toFixed(2));
.duration(fieldController.writeValue)
.asHours()
.toFixed(2)
);
} }
scope.$watch('customEditorController.writeValue', function(value) {
if (value === null) { // The level of indirection introduced by numberValue is necessary to prevent
fieldController.writeValue = null; // a non terminating digest cycle. The alternative would be:
} else { // scope.$watch('field.value', function(newValue) {
// get rounded minutes so that we don't have to send 12.223000000003 // ...
// to the server // field.value = calculatedValue;
var minutes = Number(moment.duration(value, 'hours').asMinutes().toFixed(2)); // });
fieldController.writeValue = moment.duration(minutes, 'minutes'); // This would mean that we change the value we are watching inside the function to be called
// upon changes.
//
// The indirection fixes it but it might break two-way-binding. If someone where to change
// field.value from the outside, this would not be reflected by numberValue.
scope.$watch('numberValue', function(newValue) {
if(newValue) {
var minutes = Number(moment.duration(newValue, 'hours').asMinutes().toFixed(2));
field.value = moment.duration(minutes, 'minutes');
} }
}); });
} }
}; };
}; }

@ -0,0 +1,3 @@
<div class="type-wrapper">
<inplace-editor-drop-down></inplace-editor-drop-down>
</div>

@ -26,27 +26,26 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function(EditableFieldsState) { angular
.module('openproject.inplace-edit')
.directive('inplaceEditorType', inplaceEditorType);
function inplaceEditorType(EditableFieldsState, FocusHelper, WorkPackageService) {
return { return {
restrict: 'E', restrict: 'E',
transclude: true, transclude: true,
replace: true, replace: true,
scope: {}, require: '^workPackageField',
require: ['^inplaceEditorDisplayPane', '^workPackageField'], templateUrl: '/components/inplace-edit/directives/field-edit/edit-type/' +
templateUrl: '/templates/work_packages/inplace_editor/custom/display/spent_time.html', 'edit-type.directive.html',
controller: function() {
this.isLinkViewable = function() {
return EditableFieldsState.workPackage.links.timeEntries;
};
this.getPath = function() { link: function(scope, element, attrs, fieldController) {
return EditableFieldsState.workPackage.links.timeEntries.href; scope.$watch('field.value.props', function(newValue, oldValue) {
}; if (newValue.hrefTracker !== oldValue.hrefTracker) {
}, scope.$emit('form.updateRequired');
controllerAs: 'customEditorController', }
link: function(scope, element, attrs, controllers) { });
scope.displayPaneController = controllers[0];
scope.fieldController = controllers[1];
} }
}; };
}; }
inplaceEditorType.$inject = ['EditableFieldsState', 'FocusHelper', 'WorkPackageService'];

@ -0,0 +1,24 @@
<div class="textarea-wrapper" ng-class="{'-preview': customEditorController.isPreview}">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<textarea
wiki-toolbar
style="min-height: 38px"
msd-elastic="\n"
class="focus-input inplace-edit--textarea -animated"
id="inplace-edit--write-value--{{::field.name}}"
ng-hide="customEditorController.isPreview && !fieldController.state.isBusy"
preview-toggle="customEditorController.togglePreview()"
name="value"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
ng-model="field.value.raw"
title="{{ fieldController.editTitle }}">
</textarea>
<div class="inplace-edit--preview" ng-if="customEditorController.isPreview && !fieldController.state.isBusy">
<span ng-bind-html="customEditorController.previewHtml"></span>
</div>
</div>

@ -26,42 +26,22 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
module.exports = function(TextileService, EditableFieldsState, $sce, AutoCompleteHelper, $timeout) { angular
.module('openproject.inplace-edit')
.directive('inplaceEditorWikiTextarea', inplaceEditorWikiTextarea);
function inplaceEditorWikiTextarea(AutoCompleteHelper, $timeout) {
return { return {
restrict: 'E', restrict: 'E',
transclude: true, transclude: true,
replace: true, replace: true,
scope: {}, templateUrl: '/components/inplace-edit/directives/field-edit/edit-wiki-textarea/' +
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/wiki_textarea.html', 'edit-wiki-textarea.directive.html',
controller: function($scope) {
this.isPreview = false;
this.previewHtml = '';
this.autocompletePath = '/work_packages/auto_complete.json';
this.togglePreview = function() { controller: InplaceEditorWikiTextareaController,
this.isPreview = !this.isPreview;
this.previewHtml = '';
// $scope.error = null;
if (!this.isPreview) {
return;
}
$scope.fieldController.state.isBusy = true;
TextileService
.renderWithWorkPackageContext(
EditableFieldsState.workPackage.form,
$scope.fieldController.writeValue.raw)
.then(angular.bind(this, function(r) {
this.previewHtml = $sce.trustAsHtml(r.data);
$scope.fieldController.state.isBusy = false;
}), angular.bind(this, function() {
this.isPreview = false;
$scope.fieldController.state.isBusy = false;
}));
};
},
controllerAs: 'customEditorController', controllerAs: 'customEditorController',
link: function(scope, element) { link: function(scope, element) {
scope.fieldController = scope.$parent.fieldController;
$timeout(function() { $timeout(function() {
AutoCompleteHelper.enableTextareaAutoCompletion(element.find('textarea')); AutoCompleteHelper.enableTextareaAutoCompletion(element.find('textarea'));
// set as dirty for the script to show a confirm on leaving the page // set as dirty for the script to show a confirm on leaving the page
@ -91,4 +71,37 @@ module.exports = function(TextileService, EditableFieldsState, $sce, AutoComplet
}); });
} }
}; };
}
inplaceEditorWikiTextarea.$inject = ['AutoCompleteHelper', '$timeout'];
function InplaceEditorWikiTextareaController($scope, $sce, TextileService, EditableFieldsState) {
var field = $scope.field;
this.isPreview = false;
this.previewHtml = '';
this.autocompletePath = '/work_packages/auto_complete.json';
this.togglePreview = function() {
this.isPreview = !this.isPreview;
this.previewHtml = '';
// $scope.error = null;
if (!this.isPreview) {
return;
}
$scope.fieldController.state.isBusy = true;
TextileService.renderWithWorkPackageContext(EditableFieldsState.workPackage.form,
field.value.raw)
.then(angular.bind(this, function(r) {
this.previewHtml = $sce.trustAsHtml(r.data);
$scope.fieldController.state.isBusy = false;
}), angular.bind(this, function() {
this.isPreview = false;
$scope.fieldController.state.isBusy = false;
}));
}; };
}
InplaceEditorWikiTextareaController.$inject = ['$scope', '$sce', 'TextileService',
'EditableFieldsState'];

@ -1,5 +1,5 @@
<div <div
class="inplace-edit attribute-{{ fieldController.field }}" class="inplace-edit attribute-{{ field.name }}"
ng-class="{'-busy': fieldController.state.isBusy}" ng-class="{'-busy': fieldController.state.isBusy}"
aria-busy="{{ fieldController.state.isBusy }}"> aria-busy="{{ fieldController.state.isBusy }}">
<ng-transclude></ng-transclude> <ng-transclude></ng-transclude>

@ -26,8 +26,14 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
angular.module('openproject.api') angular
.factory('HALAPIResource', ['$timeout', .module('openproject.inplace-edit')
'$q', .directive('inplaceEditorMainPane', inplaceEditorMainPane);
require('./hal-api-resource')
]); function inplaceEditorMainPane() {
return {
transclude: true,
replace: true,
templateUrl: '/components/inplace-edit/directives/main-pane/main-pane.directive.html'
};
}

@ -0,0 +1,7 @@
<div id="work-package-{{ field.name }}"
class="work-package-field work-packages--details--{{ field.name }}">
<inplace-editor-main-pane>
<inplace-editor-display-pane></inplace-editor-display-pane>
<inplace-editor-edit-pane ng-if="field.isEditable()"></inplace-editor-edit-pane>
</inplace-editor-main-pane>
</div>

@ -0,0 +1,63 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular
.module('openproject.inplace-edit')
.directive('workPackageField', workPackageField);
function workPackageField() {
return {
restrict: 'E',
replace: true,
templateUrl: '/components/inplace-edit/directives/work-package-field/' +
'work-package-field.directive.html',
scope: {
fieldName: '='
},
bindToController: true,
controller: WorkPackageFieldController,
controllerAs: 'fieldController'
};
}
function WorkPackageFieldController($scope, EditableFieldsState, inplaceEdit) {
var workPackage = EditableFieldsState.workPackage;
this.state = EditableFieldsState;
$scope.field = inplaceEdit.form(workPackage.props.id, workPackage).field(this.fieldName);
var field = $scope.field;
if (field.isEditable()) {
this.state.isBusy = false;
this.isEditing = this.state.forcedEditState;
this.editTitle = I18n.t('js.inplace.button_edit', { attribute: field.getLabel() });
}
}
WorkPackageFieldController.$inject = ['$scope', 'EditableFieldsState', 'inplaceEdit'];

@ -0,0 +1,113 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
angular
.module('openproject.inplace-edit')
.factory('inplaceEdit', inplaceEdit);
function inplaceEdit(WorkPackageFieldService) {
var forms = {};
function Form(resource) {
this.resource = resource;
this.fields = {};
this.field = function (name) {
this.fields[name] = this.fields[name] || new Field(this.resource, name);
return this.fields[name];
};
}
Object.defineProperty(Form.prototype, 'length', {
get: function () {
return Object.keys(this.fields).length;
}
});
function Field(resource, name) {
this.resource = resource;
this.name = name;
this.value = !_.isUndefined(this.value) ? this.value : _.cloneDeep(this.getValue());
}
Object.defineProperty(Field.prototype, 'text', {
get: function() {
return this.format();
}
});
// Looks up placeholders in the localization files (e.g. js-en.yml).
// The path is
// js:
// [name of the resource in snake case and pluralized]:
// placeholders:
// [name of the field]:
//
// Falls back to default if no specific placeholder is defined.
Object.defineProperty(Field.prototype, 'placeholder', {
get: function() {
if (this.resource.props._type === undefined) {
return I18n.t('js.placeholders.default');
}
// lodash does snakeCase in version 3.10
// This also pluralizes the easy way by appending 's' to the end
// which is error prone
var resourceName = this.resource.props._type
.replace(/([A-Z])/g, function($1){return '_' + $1.toLowerCase();})
.replace(/^_/, '') + 's';
var scope = 'js.' + resourceName + '.placeholders.' + this.name;
var translation = I18n.t(scope);
if (I18n.missingTranslation(scope) === translation) {
return I18n.t('js.' + resourceName + '.placeholders.default');
}
else {
return translation;
}
}
});
_.forOwn(WorkPackageFieldService, function (property, name) {
Field.prototype[name] = _.isFunction(property) && function () {
return property(this.resource, this.name);
} || property;
});
return {
form: function (id, resource) {
forms[id] = forms[id] || new Form(resource);
return forms[id];
}
};
}
inplaceEdit.$inject = ['WorkPackageFieldService'];

@ -26,7 +26,26 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
angular.module('openproject.workPackages.directives') describe('Inplace edit service', function () {
.directive('inplaceDisplayUser', require('./inplace-display-user-directive')) var inplaceEdit,
.directive('inplaceDisplaySpentTime', require('./inplace-display-spent-time-directive')) resources = ['some object', 'some other object'],
.directive('inplaceDisplayVersion', require('./inplace-display-version-directive')); WorkPackageFieldService = {};
beforeEach(angular.mock.module('openproject.inplace-edit', function ($provide) {
$provide.constant('WorkPackageFieldService', WorkPackageFieldService);
WorkPackageFieldService.getValue = sinon.stub()
}));
beforeEach(inject(function(_inplaceEdit_) {
inplaceEdit = _inplaceEdit_;
inplaceEdit.form(1, resources[0]).field('myField');
inplaceEdit.form(2, resources[1]).field('myField');
inplaceEdit.form(2, resources[1]).field('myOtherField');
}));
it('should return correct number of fields', function () {
expect(inplaceEdit.form(1).length).to.equal(1);
expect(inplaceEdit.form(2).length).to.equal(2);
});
});

@ -26,19 +26,12 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function( angular
I18n, .module('openproject.services')
WORK_PACKAGE_REGULAR_EDITABLE_FIELD, .service('WorkPackageFieldService', WorkPackageFieldService);
WorkPackagesHelper,
$q, function WorkPackageFieldService($q, $http, $filter, I18n, WorkPackagesHelper, HookService,
$http, EditableFieldsState ) {
$rootScope,
$timeout,
$filter,
HookService,
NotificationsService,
EditableFieldsState
) {
function getSchema(workPackage) { function getSchema(workPackage) {
if (workPackage.form) { if (workPackage.form) {
@ -245,7 +238,7 @@ module.exports = function(
} }
switch(fieldType) { switch(fieldType) {
case 'DateRange': case 'DateRange':
inplaceType = 'daterange'; inplaceType = 'date-range';
break; break;
case 'Date': case 'Date':
inplaceType = 'date'; inplaceType = 'date';
@ -261,7 +254,7 @@ module.exports = function(
break; break;
case 'Formattable': case 'Formattable':
if (workPackage.form.embedded.payload.props[field].format === 'textile') { if (workPackage.form.embedded.payload.props[field].format === 'textile') {
inplaceType = 'wiki_textarea'; inplaceType = 'wiki-textarea';
} else { } else {
inplaceType = 'textarea'; inplaceType = 'textarea';
} }
@ -269,14 +262,16 @@ module.exports = function(
case 'Duration': case 'Duration':
inplaceType = 'duration'; inplaceType = 'duration';
break; break;
case 'Type':
inplaceType = 'type';
break;
case 'StringObject': case 'StringObject':
case 'Version':
case 'User': case 'User':
case 'Status': case 'Status':
case 'Priority': case 'Priority':
case 'Category': case 'Category':
case 'Type': case 'Version':
inplaceType = 'dropdown'; inplaceType = 'drop-down';
break; break;
} }
@ -315,10 +310,10 @@ module.exports = function(
displayStrategy = 'text'; displayStrategy = 'text';
break; break;
case 'SpentTime': case 'SpentTime':
displayStrategy = 'spent_time'; displayStrategy = 'spent-time';
break; break;
case 'Formattable': case 'Formattable':
displayStrategy = 'wiki_textarea'; displayStrategy = 'wiki-textarea';
break; break;
case 'Version': case 'Version':
displayStrategy = 'version'; displayStrategy = 'version';
@ -327,7 +322,7 @@ module.exports = function(
displayStrategy = 'user'; displayStrategy = 'user';
break; break;
case 'DateRange': case 'DateRange':
displayStrategy = 'daterange'; displayStrategy = 'date-range';
break; break;
case 'Date': case 'Date':
displayStrategy = 'date'; displayStrategy = 'date';
@ -377,7 +372,7 @@ module.exports = function(
updatedAt: 'datetime' updatedAt: 'datetime'
}; };
if (schema.props[field]) { if (schema.props[field] && schema.props[field]) {
if (schema.props[field].type === 'Duration') { if (schema.props[field].type === 'Duration') {
var hours = moment.duration(value).asHours(); var hours = moment.duration(value).asHours();
var formattedHours = $filter('number')(hours, 2); var formattedHours = $filter('number')(hours, 2);
@ -396,27 +391,6 @@ module.exports = function(
return WorkPackagesHelper.formatValue(value, mappings[field]); return WorkPackagesHelper.formatValue(value, mappings[field]);
} }
function submitWorkPackageChanges(callback) {
// We have to ensure that some promises are executed earlier then others
var promises = [];
angular.forEach(EditableFieldsState.submissionPromises, function(field) {
var p = field.thePromise.call(this);
if (field.prepend) {
promises.unshift(p);
} else {
promises.push(p);
}
});
$q.all(promises).then(function() {
// Update work package after this call
$rootScope.$emit('workPackageRefreshRequired', callback);
EditableFieldsState.errors = null;
EditableFieldsState.submissionPromises = {};
EditableFieldsState.currentField = null;
});
}
var WorkPackageFieldService = { var WorkPackageFieldService = {
getSchema: getSchema, getSchema: getSchema,
isEditable: isEditable, isEditable: isEditable,
@ -432,10 +406,17 @@ module.exports = function(
getAllowedValues: getAllowedValues, getAllowedValues: getAllowedValues,
format: format, format: format,
getInplaceEditStrategy: getInplaceEditStrategy, getInplaceEditStrategy: getInplaceEditStrategy,
getInplaceDisplayStrategy: getInplaceDisplayStrategy, getInplaceDisplayStrategy: getInplaceDisplayStrategy
defaultPlaceholder: '-',
submitWorkPackageChanges: submitWorkPackageChanges
}; };
return WorkPackageFieldService; return WorkPackageFieldService;
}; }
WorkPackageFieldService.$inject = [
'$q',
'$http',
'$filter',
'I18n',
'WorkPackagesHelper',
'HookService',
'EditableFieldsState'];

@ -173,6 +173,9 @@ angular.module('openproject.api', []);
angular.module('openproject.templates', []); angular.module('openproject.templates', []);
// refactoring
angular.module('openproject.inplace-edit', []);
// main app // main app
var openprojectApp = angular.module('openproject', [ var openprojectApp = angular.module('openproject', [
'ui.date', 'ui.date',
@ -192,7 +195,8 @@ var openprojectApp = angular.module('openproject', [
'cgBusy', 'cgBusy',
'openproject.api', 'openproject.api',
'openproject.templates', 'openproject.templates',
'monospaced.elastic' 'monospaced.elastic',
'openproject.inplace-edit'
]); ]);
window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') || window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') ||
@ -271,8 +275,6 @@ openprojectApp
} }
]); ]);
require('./api');
angular.module('openproject.config') angular.module('openproject.config')
.service('ConfigurationService', [ .service('ConfigurationService', [
'PathHelper', 'PathHelper',
@ -296,3 +298,6 @@ var requireTemplate = require.context('./templates', true, /\.html$/);
requireTemplate.keys().forEach(requireTemplate); requireTemplate.keys().forEach(requireTemplate);
require('!ngtemplate?module=openproject.templates!html!angular-busy/angular-busy.html'); require('!ngtemplate?module=openproject.templates!html!angular-busy/angular-busy.html');
var requireComponent = require.context('./components/', true, /^((?!\.(test|spec)).)*\.(js|html)$/);
requireComponent.keys().forEach(requireComponent);

@ -1 +0,0 @@
<span ng-bind="displayPaneController.getReadValue()"></span>

@ -1 +0,0 @@
<op-date date-value="displayPaneController.getReadValue()" no-date-text="displayPaneController.placeholder"></op-date>

@ -1 +0,0 @@
<op-date date-value="displayPaneController.getReadValue().startDate" no-date-text="displayPaneController.getReadValue().noStartDate"></op-date>&nbsp;&nbsp;-&nbsp;&nbsp;<op-date date-value="displayPaneController.getReadValue().dueDate" no-date-text="displayPaneController.getReadValue().noEndDate"></op-date>

@ -1 +0,0 @@
<span ng-bind="displayPaneController.getReadValue().props.name || displayPaneController.getReadValue().props.value || displayPaneController.placeholder"></span>

@ -1,2 +0,0 @@
<span ng-if="displayPaneController.isReadValueEmpty()" ng-bind="displayPaneController.placeholder"></span>
<span ng-if="!displayPaneController.isReadValueEmpty()" ng-bind="displayPaneController.getReadValue()"></span>

@ -1 +0,0 @@
<span ng-bind-html="displayPaneController.getReadValue().html || displayPaneController.placeholder"></span>

@ -1,10 +0,0 @@
<div class="switch">
<input type="checkbox"
class="focus-input"
id="checkbox-switch-{{ fieldController.field }}"
name="value"
ng-disabled="fieldController.state.isBusy"
title="{{ fieldController.editTitle }}"
ng-model="fieldController.writeValue" />
<label for="checkbox-switch-{{ fieldController.field }}" title="{{ fieldController.editTitle }}"></label>
</div>

@ -1 +0,0 @@
<inplace-editor-dropdown></inplace-editor-dropdown>

@ -1,8 +0,0 @@
<input class="focus-input inplace-edit--text-field"
name="value"
type="number"
step="0.01"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="fieldController.writeValue" />

@ -1,7 +0,0 @@
<input class="focus-input inplace-edit--text-field"
name="value"
type="number"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="fieldController.writeValue" />

@ -1,7 +0,0 @@
<input class="focus-input inplace-edit--text-field"
name="value"
type="text"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="fieldController.writeValue" />

@ -0,0 +1 @@
<op-date date-value="field.text.startDate" no-date-text="field.text.noStartDate"></op-date>&nbsp;&nbsp;-&nbsp;&nbsp;<op-date date-value="field.text.dueDate" no-date-text="field.text.noEndDate"></op-date>

@ -0,0 +1 @@
<op-date date-value="field.text" no-date-text="field.placeholder"></op-date>

@ -1,5 +1,5 @@
<work-package-dynamic-attribute <work-package-dynamic-attribute
html-element="displayPaneController.getDynamicDirectiveName()" html-element="displayPaneController.getDynamicDirectiveName()"
work-package="displayPaneController.getWorkPackage()" field="field"
class="dynamic-attribute"> class="dynamic-attribute">
</work-package-dynamic-attribute> </work-package-dynamic-attribute>

@ -0,0 +1 @@
<span ng-bind="field.text.props.name || field.text.props.value || field.placeholder"></span>

@ -0,0 +1,2 @@
<span ng-if="field.isEmpty()" ng-bind="field.placeholder"></span>
<span ng-if="!field.isEmpty()" ng-bind="field.text"></span>

@ -0,0 +1 @@
<span ng-bind-html="field.text.html || field.placeholder"></span>

@ -0,0 +1,14 @@
<div class="switch">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<input type="checkbox"
class="focus-input"
id="inplace-edit--write-value--{{::field.name}}"
name="value"
ng-disabled="fieldController.state.isBusy"
title="{{ fieldController.editTitle }}"
ng-model="field.value" />
</div>

@ -0,0 +1 @@
<inplace-editor-drop-down></inplace-editor-drop-down>

@ -0,0 +1,16 @@
<div class="float-wrapper">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<input class="focus-input inplace-edit--text-field"
id="inplace-edit--write-value--{{::field.name}}"
name="value"
type="number"
step="0.01"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="field.value" />
</div>

@ -0,0 +1,15 @@
<div class="integer-wrapper">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<input class="focus-input inplace-edit--text-field"
id="inplace-edit--write-value--{{::field.name}}"
name="value"
type="number"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="field.value" />
</div>

@ -0,0 +1,15 @@
<div class="text-wrapper">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}">
{{::field.getLabel()}}
</label>
<input class="focus-input inplace-edit--text-field"
id="inplace-edit--write-value--{{::field.name}}"
name="value"
type="text"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="field.value" />
</div>

@ -3,6 +3,6 @@
name="value" name="value"
ng-disabled="fieldController.state.isBusy" ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired" ng-required="fieldController.isRequired"
ng-model="fieldController.writeValue.raw" ng-model="field.value.raw"
title="{{ fieldController.editTitle }}"> title="{{ fieldController.editTitle }}">
</textarea> </textarea>

@ -0,0 +1 @@
<inplace-editor-type></inplace-editor-type>

@ -27,7 +27,7 @@
</span> </span>
<div class="work-packages--details--title"> <div class="work-packages--details--title">
<work-package-field field="'subject'"></work-package-field> <work-package-field field-name="'subject'"></work-package-field>
</div> </div>
<div class="work-package-details-tab" ui-view></div> <div class="work-package-details-tab" ui-view></div>
@ -35,6 +35,6 @@
</div> </div>
<div class="bottom-toolbar"> <div class="bottom-toolbar">
<work-package-details-toolbar work-package='workPackage'> <work-package-details-toolbar work-package='workPackage'></work-package-details-toolbar>
</work-package-details-toolbar> <work-package-edit-actions></work-package-edit-actions>
</div> </div>

@ -8,11 +8,11 @@
<ul class="toolbar-items"> <ul class="toolbar-items">
<li class="toolbar-item"> <li class="toolbar-item">
<button class="button -alt-highlight" <button class="button -alt-highlight add-work-package"
has-dropdown-menu has-dropdown-menu
target="TasksDropdownMenu" target="TasksDropdownMenu"
locals="availableTypes,projectIdentifier" locals="availableTypes,projectIdentifier"
ng-disabled="cannot('work_package', 'create')"> ng-disabled="cannot('work_package', 'create') || editAll.state">
<i class="button--icon icon-add"></i> <i class="button--icon icon-add"></i>
<span class="button--text" ng-bind="::I18n.t('js.toolbar.unselected_title')"></span> <span class="button--text" ng-bind="::I18n.t('js.toolbar.unselected_title')"></span>
<i class="button--dropdown-indicator"></i> <i class="button--dropdown-indicator"></i>
@ -23,6 +23,7 @@
{{ getToggleActionLabel(showFiltersOptions) + ' ' + I18n.t('js.button_filter') }} {{ getToggleActionLabel(showFiltersOptions) + ' ' + I18n.t('js.button_filter') }}
</label> </label>
<button id="work-packages-filter-toggle-button" <button id="work-packages-filter-toggle-button"
ng-disabled="editAll.state"
class="button" class="button"
title="{{ getToggleActionLabel(showFiltersOptions) + ' ' + I18n.t('js.button_filter') }}" title="{{ getToggleActionLabel(showFiltersOptions) + ' ' + I18n.t('js.button_filter') }}"
ng-click="toggleShowFilterOptions()" ng-click="toggleShowFilterOptions()"
@ -41,6 +42,7 @@
{{ getActivationActionLabel(isDetailsViewActive()) + ' ' + I18n.t('js.button_list_view') }} {{ getActivationActionLabel(isDetailsViewActive()) + ' ' + I18n.t('js.button_list_view') }}
</label> </label>
<button id="work-packages-list-view-button" <button id="work-packages-list-view-button"
ng-disabled="editAll.state"
class="button" class="button"
title="{{ getActivationActionLabel(isDetailsViewActive()) + ' ' + I18n.t('js.button_list_view') }}" title="{{ getActivationActionLabel(isDetailsViewActive()) + ' ' + I18n.t('js.button_list_view') }}"
ng-click="closeDetailsView()" ng-click="closeDetailsView()"
@ -85,6 +87,7 @@
{{ I18n.t('js.button_settings') }} {{ I18n.t('js.button_settings') }}
</label> </label>
<button id="work-packages-settings-button" <button id="work-packages-settings-button"
ng-disabled="editAll.state"
title="{{ I18n.t('js.button_settings') }}" title="{{ I18n.t('js.button_settings') }}"
class="button last work-packages-settings-button" class="button last work-packages-settings-button"
has-dropdown-menu has-dropdown-menu
@ -107,7 +110,8 @@
<back-url></back-url> <back-url></back-url>
<div class="work-packages--split-view" cg-busy="[settingUpPage,refreshWorkPackages]"> <div class="work-packages--split-view" cg-busy="[settingUpPage,refreshWorkPackages]"
ng-class="{'edit-all-mode': editAll.state}">
<div class="work-packages--list"> <div class="work-packages--list">
<div class="work-packages--list-table-area"> <div class="work-packages--list-table-area">
<work-packages-table ng-if="rows && columns" <work-packages-table ng-if="rows && columns"

@ -3,7 +3,7 @@
<h2>{{ ::I18n.t('js.work_packages.create.header') }}</h2> <h2>{{ ::I18n.t('js.work_packages.create.header') }}</h2>
<div class="work-packages--create--title"> <div class="work-packages--create--title">
<work-package-field field="'subject'" tabindex="0"></work-package-field> <work-package-field field-name="'subject'" tabindex="0"></work-package-field>
</div> </div>
<div class="attributes-group--header"> <div class="attributes-group--header">
@ -15,7 +15,7 @@
</div> </div>
<div class="single-attribute wiki"> <div class="single-attribute wiki">
<work-package-field field="'description'"></work-package-field> <work-package-field field-name="'description'"></work-package-field>
</div> </div>
<div ng-repeat="group in vm.groupedFields" ng-hide="vm.hideEmptyFields && vm.isGroupHideable(vm.groupedFields, group.groupName, vm.workPackage)" class="attributes-group"> <div ng-repeat="group in vm.groupedFields" ng-hide="vm.hideEmptyFields && vm.isGroupHideable(vm.groupedFields, group.groupName, vm.workPackage)" class="attributes-group">
@ -47,7 +47,7 @@
ng-if="vm.isSpecified(vm.workPackage, field) && vm.isEditable(vm.workPackage, field)" ng-if="vm.isSpecified(vm.workPackage, field) && vm.isEditable(vm.workPackage, field)"
ng-repeat-end ng-repeat-end
class="attributes-key-value--value-container"> class="attributes-key-value--value-container">
<work-package-field field="field"></work-package-field> <work-package-field field-name="field"></work-package-field>
</dd> </dd>
</dl> </dl>
</div> </div>

@ -1,27 +1,29 @@
<div class="work-packages--show-view"> <div class="work-packages--show-view" ng-class="{'edit-all-mode': editAll.state}">
<div class="toolbar-container"> <div class="toolbar-container">
<div toolbar id="toolbar"> <div toolbar id="toolbar">
<ul id="toolbar-items"> <ul id="toolbar-items">
<li class="toolbar-item" ng-hide="true"> <li class="toolbar-item" ng-hide="true">
<button class="button -alt-highlight" <button class="button -alt-highlight add-work-package"
has-dropdown-menu has-dropdown-menu
target="TasksDropdownMenu" target="TasksDropdownMenu"
locals="availableTypes,projectIdentifier" locals="availableTypes,projectIdentifier"
ng-disabled="cannot('work_package', 'create')"> ng-disabled="cannot('work_package', 'create') || editAll.state">
<i class="button--icon icon-add"></i> <i class="button--icon icon-add"></i>
<span class="button--text" ng-bind="::I18n.t('js.toolbar.unselected_title')"></span> <span class="button--text" ng-bind="::I18n.t('js.toolbar.unselected_title')"></span>
<i class="button--dropdown-indicator"></i> <i class="button--dropdown-indicator"></i>
</button> </button>
</li> </li>
<li class="toolbar-item"> <li class="toolbar-item">
<button class="button" <button class="button edit-all-button"
ng-click="editWorkPackage()" ng-hide="editAll.state"
ng-click="editAll.start()"
ng-disabled="!editAll.allowed"
title="{{I18n.t('js.button_edit')}}"> title="{{I18n.t('js.button_edit')}}">
<i class="button--icon icon-edit"></i> <i class="button--icon icon-edit"></i>
</button> </button>
</li> </li>
<li class="toolbar-item" ng-if="displayWatchButton"> <li class="toolbar-item" ng-if="displayWatchButton">
<work-package-watcher-button work-package="workPackage"></work-package-watcher-button> <work-package-watcher-button work-package="workPackage" disabled="editAll.state"></work-package-watcher-button>
</li> </li>
<li class="toolbar-item" feature-flag="detailsView"> <li class="toolbar-item" feature-flag="detailsView">
<ul id="work-packages-view-mode-selection" class="toolbar-button-group"> <ul id="work-packages-view-mode-selection" class="toolbar-button-group">
@ -32,6 +34,7 @@
{{ getActivationActionLabel(isDetailsViewActive()) + ' ' + I18n.t('js.button_list_view') }} {{ getActivationActionLabel(isDetailsViewActive()) + ' ' + I18n.t('js.button_list_view') }}
</label> </label>
<button id="work-packages-list-view-button" <button id="work-packages-list-view-button"
ng-disabled="editAll.state"
class="button" class="button"
title="{{ getActivationActionLabel(!isListViewActive()) + ' ' + I18n.t('js.button_list_view') }}" title="{{ getActivationActionLabel(!isListViewActive()) + ' ' + I18n.t('js.button_list_view') }}"
ng-click="closeShowView()" ng-click="closeShowView()"
@ -73,7 +76,7 @@
</li> </li>
<li class="toolbar-item action_menu_main" id="action-show-more-dropdown-menu"> <li class="toolbar-item action_menu_main" id="action-show-more-dropdown-menu">
<button class="button dropdown-relative" <button class="button dropdown-relative"
ng-disabled="!actionsAvailable" ng-disabled="!actionsAvailable || editAll.state"
has-dropdown-menu has-dropdown-menu
target="ShowMoreDropdownMenu" target="ShowMoreDropdownMenu"
locals="permittedActions,actionsAvailable,triggerMoreMenuAction"> locals="permittedActions,actionsAvailable,triggerMoreMenuAction">
@ -85,7 +88,7 @@
<ul class="subject-header"> <ul class="subject-header">
<li class="subject-header-inner"> <li class="subject-header-inner">
<div class="inline-edit"> <div class="inline-edit">
<work-package-field field="'subject'"></work-package-field> <work-package-field field-name="'subject'"></work-package-field>
</div> </div>
</li> </li>
</ul> </ul>
@ -116,7 +119,7 @@
</div> </div>
<div class="single-attribute wiki"> <div class="single-attribute wiki">
<work-package-field field="'description'"></work-package-field> <work-package-field field-name="'description'"></work-package-field>
</div> </div>
</div> </div>
@ -149,12 +152,15 @@
ng-if="vm.isSpecified(vm.workPackage, field)" ng-if="vm.isSpecified(vm.workPackage, field)"
ng-repeat-end ng-repeat-end
class="attributes-key-value--value-container"> class="attributes-key-value--value-container">
<work-package-field field="field"></work-package-field> <work-package-field field-name="field"></work-package-field>
</dd> </dd>
</dl> </dl>
</div> </div>
<work-package-attachments edit data-ng-show="!vm.hideEmptyFields || vm.filesExist" work-package="vm.workPackage"></work-package-attachments> <work-package-attachments edit data-ng-show="!vm.hideEmptyFields || vm.filesExist" work-package="vm.workPackage"></work-package-attachments>
<work-package-edit-actions></work-package-edit-actions>
</div> </div>
</div> </div>
<div class="work-packages--right-panel"> <div class="work-packages--right-panel">

@ -17,8 +17,14 @@
</accessible-by-keyboard> </accessible-by-keyboard>
</div> </div>
<div class="inplace-edit--write edit-strategy-comment" ng-show="fieldController.isEditing"> <div class="inplace-edit--write edit-strategy-comment" ng-show="fieldController.isEditing">
<form class="inplace-edit--form" ng-if="fieldController.isEditing" name="fieldController.editForm" ng-submit="fieldController.submit()" novalidate> <form class="inplace-edit--form"
<div class="inplace-edit--write-value" ng-include="'/templates/components/inplace_editor/editable/wiki_textarea.html'" ng-click="fieldController.markActive()" tabindex="-1"> ng-if="fieldController.isEditing"
name="fieldController.editForm"
ng-submit="fieldController.submit()"
novalidate>
<div class="inplace-edit--write-value"
tabindex="-1">
<inplace-editor-wiki-textarea> </inplace-editor-wiki-textarea>
</div> </div>
<div class="inplace-edit--dashboard"> <div class="inplace-edit--dashboard">
<div class="inplace-edit--controls" ng-hide="fieldController.state.isBusy || !fieldController.isActive()"> <div class="inplace-edit--controls" ng-hide="fieldController.state.isBusy || !fieldController.isActive()">

@ -1,7 +0,0 @@
<div id="work-package-{{ fieldController.field }}"
class="work-package-field work-packages--details--{{ fieldController.field }}">
<inplace-editor-main-pane>
<inplace-editor-display-pane></inplace-editor-display-pane>
<inplace-editor-edit-pane ng-if="fieldController.isEditable()"></inplace-editor-edit-pane>
</inplace-editor-main-pane>
</div>

@ -1,14 +0,0 @@
<span class="user-avatar--container">
<img class="user-avatar--avatar"
ng-if="customEditorController.getUserName() && customEditorController.getUser().props.avatar"
ng-src="{{customEditorController.getUser().props.avatar}}" alt="Avatar" title="{{customEditorController.getUserName()}}" />
<span class="user-avatar--user-with-role">
<span class="user-avatar--user" ng-if="customEditorController.getUserName()">
<a ng-if="customEditorController.getUser().props.subtype !== 'Group'" ng-href="{{ customEditorController.userPath(customEditorController.getUser().props.id) }}" ng-bind="customEditorController.getUser().props.name"
class="user-field-user-link"/>
<span ng-if="customEditorController.getUser().props.subtype == 'Group'" ng-bind="customEditorController.getUser().props.name" class="user-field-user-link"/>
</span>
<span class="user-avatar--user" ng-if="!customEditorController.getUserName()"> - </span>
<span class="user-avatar--role" ng-if="customEditorController.getUser().props.role" ng-bind="customEditorController.getUser().props.role"/>
</span>
</span>

@ -1,11 +0,0 @@
<div class="version-wrapper">
<span ng-if="!displayPaneController.getReadValue()">-</span>
<span ng-if="displayPaneController.getReadValue() && customEditorController.isVersionLinkViewable()">
<a href="{{customEditorController.pathHelper.staticVersionPath(displayPaneController.getReadValue().props.id)}}">
{{displayPaneController.getReadValue().props.name}}
</a>
</span>
<span ng-if="displayPaneController.getReadValue() && !customEditorController.isVersionLinkViewable()">
{{displayPaneController.getReadValue().props.name}}
</span>
</div>

@ -1,9 +0,0 @@
<div class="inplace-edit--date">
<input ng-model="fieldController.writeValue"
ng-change="onEdit()"
ng-click="showDatepicker()"
title="{{ fieldController.editTitle }}"
class="inplace-edit--date"
type="text" />
<div class="inplace-edit--date-picker"></div>
</div>

@ -1,16 +0,0 @@
<div class="dropdown-wrapper">
<ui-select
class="inplace-edit--select"
name="value"
ng-disabled="fieldController.state.isBusy"
ng-model="fieldController.writeValue.props.href"
title="{{ fieldController.editTitle }}"
reset-search-input="true"
theme="select2">
<ui-select-match>{{ $select.selected.name }}</ui-select-match>
<ui-select-choices
repeat="item.href as item in customEditorController.allowedValues | filter: $select.search">
<div aria-label="{{ item.name || customEditorController.nullValueLabel }}" ng-bind-html="item.name | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>

@ -1,8 +0,0 @@
<input class="focus-input inplace-edit--text-field"
name="value"
type="number"
step="0.5"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
title="{{ fieldController.editTitle }}"
ng-model="customEditorController.writeValue" />

@ -1,18 +0,0 @@
<div class="textarea-wrapper" ng-class="{'-preview': customEditorController.isPreview}">
<textarea
wiki-toolbar
style="min-height: 38px"
msd-elastic="\n"
class="focus-input inplace-edit--textarea -animated"
ng-hide="customEditorController.isPreview && !fieldController.state.isBusy"
preview-toggle="customEditorController.togglePreview()"
name="value"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
ng-model="fieldController.writeValue.raw"
title="{{ fieldController.editTitle }}">
</textarea>
<div class="inplace-edit--preview" ng-if="customEditorController.isPreview && !fieldController.state.isBusy">
<span ng-bind-html="customEditorController.previewHtml"></span>
</div>
</div>

@ -13,7 +13,7 @@
</div> </div>
<div class="single-attribute wiki"> <div class="single-attribute wiki">
<work-package-field field="'description'"></work-package-field> <work-package-field field-name="'description'"></work-package-field>
</div> </div>
</div> </div>
@ -46,7 +46,7 @@
ng-if="vm.isSpecified(vm.workPackage, field)" ng-if="vm.isSpecified(vm.workPackage, field)"
ng-repeat-end ng-repeat-end
class="attributes-key-value--value-container"> class="attributes-key-value--value-container">
<work-package-field field="field"></work-package-field> <work-package-field field-name="field"></work-package-field>
</dd> </dd>
</dl> </dl>
</div> </div>

@ -1,6 +1,7 @@
<button class="button" <button class="button"
ng-click="toggleWatch()" ng-click="toggleWatch()"
title="{{ buttonTitle }}" title="{{ buttonTitle }}"
ng-disabled="disabled"
id="{{ buttonId }}"> id="{{ buttonId }}">
<i class="button--icon" ng-class="watchIconClass"></i> <i class="button--icon" ng-class="watchIconClass"></i>
<span class="button--text" <span class="button--text"

@ -1,12 +1,11 @@
<div class="work-packages--details-toolbar"> <div class="work-packages--details-toolbar" ng-hide="editAll.state && !editAll.allowed">
<button class="button" <button class="button" accesskey="3" ng-click="editAll.start()" ng-disabled="!editAll.allowed">
accesskey="3"
ng-click="editWorkPackage()">
<i class="button--icon icon-edit"></i> <i class="button--icon icon-edit"></i>
<span class="button--text" ng-bind="::I18n.t('js.button_edit')"></span> <span class="button--text" ng-bind="::I18n.t('js.button_edit')"></span>
</button> </button>
<work-package-watcher-button work-package="workPackage" <work-package-watcher-button work-package="workPackage"
show-text="true" show-text="true"
disabled="editAll.state"
ng-if="displayWatchButton"> ng-if="displayWatchButton">
</work-package-watcher-button> </work-package-watcher-button>
<button class="button dropdown-relative" <button class="button dropdown-relative"

@ -0,0 +1,10 @@
<div class="work-packages--edit-actions" ng-show="efs.editAll.state && efs.editAll.allowed">
<button class="button -alt-highlight" accesskey="3" ng-click="efs.save()">
<i class="button--icon icon-yes"></i>
<span class="button--text" ng-bind="::I18n.t('js.button_save')"></span>
</button>
<button class="button" accesskey="7" ng-click="efs.editAll.cancel()">
<i class="button--icon icon-close"></i>
<span class="button--text" ng-bind="::I18n.t('js.button_cancel')"></span>
</button>
</div>

@ -174,6 +174,7 @@ angular.module('openproject.workPackages.controllers')
'Query', 'Query',
'OPERATORS_AND_LABELS_BY_FILTER_TYPE', 'OPERATORS_AND_LABELS_BY_FILTER_TYPE',
'NotificationsService', 'NotificationsService',
'EditableFieldsState',
require('./work-packages-list-controller') require('./work-packages-list-controller')
]) ])
.factory('columnsModal', ['btfModal', function(btfModal) { .factory('columnsModal', ['btfModal', function(btfModal) {

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

Loading…
Cancel
Save