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 {
margin-top: 15px;
}
.button.icon-edit.ng-hide {
display: block !important;
visibility: hidden;
}
}
.nosidebar {

@ -178,6 +178,18 @@
.work-packages--attachments
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
margin-bottom: 1rem

@ -163,7 +163,6 @@ en:
label_global_queries: "Shared queries"
label_custom_queries: "Private queries"
label_columns: "Columns"
label_click_to_enter_description: "Click to enter description..."
label_attachments: Files
label_drop_files: Drop files here
label_drop_files_hint: or click to add files
@ -180,6 +179,9 @@ en:
label_successful_update: 'Successful update'
label_validation_error: "The work package could not be saved due to the following errors:"
placeholders:
default: '-'
text_are_you_sure: "Are you sure?"
watchers:
@ -386,6 +388,9 @@ en:
updatedAt: "Updated on"
versionName: "Version"
version: "Version"
placeholders:
default: "-"
description: "Click to enter description..."
query:
column_names: "Columns"
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":
| subject |
| wp1 |
And I am admin
And I am already admin
@javascript @selenium
Scenario: Searching stuff retains a project's scope
@ -43,5 +43,5 @@ Feature: Searching
And I search for "wp1" after having searched
Then I should see "Overview" within "#main-menu"
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"

@ -35,10 +35,16 @@ Then /^there should( not)? be an(?:y)? error message$/ do |no_message|
end
end
# This one aims at the rails flash based errors
Then /^I should see an error explanation stating "([^"]*)"$/ do |message|
page.all(:css, '.errorExplanation li, .errorExplanation li *', text: message).should_not be_empty
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|
if no_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
# to identify different changesets
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
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

@ -133,9 +133,7 @@ Then /^the work package should be shown with the following values:$/ do |table|
end
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: expected_header)
should have_css('.subject-header', text: table.rows_hash['Subject'])
end
if table.rows_hash['Description']
@ -172,3 +170,30 @@ When /^I click the unwatch work package button$/ do
find('#unwatch-button').click
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)
"/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)
"/issues/#{issue.id}/edit"
"/work_packages/#{issue.id}/activity"
when /^the copy page (?:for|of) the work package "([^\"]+)"$/
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 there are the following work packages in project "ecookbook":
| subject | start_date | due_date |
| pe1 | 2013-01-01 | 2013-12-31 |
And the work package "pe1" has the following changesets:
| wp1 | 2013-01-01 | 2013-12-31 |
And the work package "wp1" has the following changesets:
| revision | committer | committed_on | comments | commit_date |
| 1 | manager | 2013-02-01 | blubs | 2013-02-01 |
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:
| view_work_packages |
| 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:
| revision | comments |
| 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
Given the role "manager" may have the following rights:
| 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

@ -32,6 +32,8 @@ Feature: Fields editable on work package edit
| login | manager |
| firstname | the |
| lastname | manager |
And the user "manager" has the following preferences
| warn_on_leaving_unsaved | false |
And there is a role "manager"
And there is 1 project with the following:
| 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 |
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:
| Type | Phase |
@ -76,34 +80,14 @@ Feature: Fields editable on work package edit
| Version | version1 |
| Start date | 2013-01-01 |
| Due date | 2013-12-31 |
| Estimated time | 5.00 |
| Progress (%) | 30 % |
| Notes | |
And the "Parent" field should contain the id of work package "parentpe"
| Estimated time | 5 |
| Progress (%) | 30 |
When I click on "Relations"
Scenario: Going to the page and viewing timelog fields if this module is enabled
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 |
Then I should see "parentpe" within ".relation[title='Parent']"
@javascript
Scenario: Going to the page and viewing custom field fields
Given the role "manager" may have the following rights:
| 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"
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:
| cf1 | 4 |

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

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

@ -46,6 +46,8 @@ Feature: Switching types of work packages
| login | bob |
| firstname | Bob |
| 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"
Given the user "bob" has 1 issue with the following:
| subject | wp1 |
@ -56,6 +58,8 @@ Feature: Switching types of work packages
@javascript
Scenario: Switching type should keep the inserted value
When I go to the edit page of the work package "wp1"
And I click the edit work package button
And I click on "Show all"
And I fill in the following:
| Responsible | Bob Bobbit |
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"
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:
| cfAll | 5 |
And I select "Feature" from "Type"

@ -32,6 +32,8 @@ Feature: Updating work packages
| login | manager |
| firstname | the |
| lastname | manager |
And the user "manager" has the following preferences
| warn_on_leaving_unsaved | false |
And there are the following types:
| Name | Is milestone |
| Phase1 | false |
@ -68,14 +70,14 @@ Feature: Updating work packages
And the type "Phase2" has the default workflow for the role "manager"
And there are the following work packages in project "ecookbook":
| subject | type | status | fixed_version |
| pe1 | Phase1 | status1 | version1 |
| pe2 | | | |
| wp1 | Phase1 | status1 | version1 |
And I am already logged in as "manager"
@javascript @wip
@javascript
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 "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:
| Type | Phase2 |
# This is to be removed once the bug
@ -88,13 +90,13 @@ Feature: Updating work packages
| Start date | 2013-03-04 |
| Due date | 2013-03-06 |
| Estimated time | 5.00 |
| Progress (%) | 30 % |
| Progress (%) | 30 |
| Priority | prio2 |
| Status | status2 |
| Subject | New subject |
| Description | Desc2 |
And I fill in the id of work package "pe2" into "Parent"
And I submit the form by the "Submit" button
And I submit the form by the "Save" button
Then I should see "Successful update"
Then I should be on the page of the work package "New subject"
And the work package should be shown with the following values:
| Responsible | the manager |
@ -107,24 +109,47 @@ Feature: Updating work packages
| Subject | New subject |
| Type | Phase2 |
| Description | Desc2 |
# And the work package "pe2" should be shown as the parent
@javascript
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:
| 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 |
And I submit the form by the "Submit" button
Then I should see "Information has been updated by at least one other user in the meantime."
And I should see "The update(s) came from"
And I submit the form by the "Save" button
Then I should see an error notification stating "Couldn't update the resource because of conflicting modifications."
@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
Scenario: Adding a note
When I go to the edit page of the work package called "pe1"
And I fill in "Notes" with "Note message"
And I submit the form by the "Submit" button
Then I should be on the page of the work package "pe1"
And I should see a journal with the following:
| Notes | Note message |
Scenario: On a work package with children a user should not be able to change attributes which are overridden by children
And there are the following work packages in project "ecookbook":
| subject | type | status | fixed_version | priority | done_ratio | estimated_hours | start_date | due_date |
| child | Phase1 | status1 | version1 | prio2 | 50 | 5 | 2015-10-01 | 2015-10-30 |
| parent | |   |   |   | 0 |   | | |
Given the work package "parent" has the following children:
| 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.
// ++
/*jshint expr: true*/
describe('HALAPIResource', function() {
var HALAPIResource, testPathHelper;
beforeEach(module('openproject.api'));
beforeEach(function() {
testPathHelper = {
apiV3: '/api/v3',
appBasePath: ''
};
module(function($provide) {
$provide.value('PathHelper', testPathHelper);
});
var HALAPIResource;
beforeEach(angular.mock.module('openproject.api'));
inject(function(_HALAPIResource_) {
beforeEach(inject(function(_HALAPIResource_) {
HALAPIResource = _HALAPIResource_;
});
});
}));
describe('setup', function() {
var apiResource, resourceFunction;

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

@ -26,88 +26,42 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
module.exports = function(
WorkPackageFieldService,
EditableFieldsState,
$timeout,
HookService,
I18n) {
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorDisplayPane', inplaceEditorDisplayPane);
function inplaceEditorDisplayPane(EditableFieldsState, $timeout) {
return {
replace: true,
transclude: true,
scope: {},
require: '^workPackageField',
templateUrl: '/templates/work_packages/inplace_editor/display_pane.html',
controller: function($scope) {
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;
};
},
templateUrl: '/components/inplace-edit/directives/display-pane/display-pane.directive.html',
controller: InplaceEditorDisplayPaneController,
controllerAs: 'displayPaneController',
link: function(scope, element, attrs, fieldController) {
var field = scope.field;
scope.fieldController = fieldController;
scope.displayPaneController.field = scope.fieldController.field;
scope.editableFieldsState = EditableFieldsState;
scope.$watchCollection('editableFieldsState.workPackage.form', function() {
var strategy = WorkPackageFieldService.getInplaceDisplayStrategy(
EditableFieldsState.workPackage,
fieldController.field
);
var strategy = field.getInplaceDisplayStrategy();
if (strategy !== scope.displayStrategy) {
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) {
if (errors) {
if (errors[scope.fieldController.field]) {
if (errors && errors[field.name] && field.isEditable()) {
scope.displayPaneController.startEditing();
}
}
}, true);
scope.$watch('fieldController.isEditing', function(isEditing, oldIsEditing) {
if (!isEditing) {
if (!isEditing && !fieldController.lockFocus) {
$timeout(function() {
if (oldIsEditing) {
// 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">
<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 class="inplace-edit--dashboard">
<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">
<span ng-if="fieldController.isEmpty()">{{ displayPaneController.placeholder }}</span>
<span ng-if="!fieldController.isEmpty()">
<span ng-if="field.isEmpty()">{{ field.placeholder }}</span>
<span ng-if="!field.isEmpty()">
<span ng-if="customEditorController.isLinkViewable()">
<a href="{{ customEditorController.getPath() }}">
{{ displayPaneController.getReadValue() }}
{{ field.text }}
</a>
</span>
<span ng-if="!customEditorController.isLinkViewable()">
{{ displayPaneController.getReadValue() }}
{{ field.text }}
</span>
</span>
</div>

@ -26,24 +26,32 @@
// 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 {
restrict: 'E',
transclude: true,
replace: true,
scope: {},
require: '^inplaceEditorDisplayPane',
templateUrl: '/templates/work_packages/inplace_editor/custom/display/version.html',
controller: function($scope) {
this.pathHelper = PathHelper;
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;
templateUrl: '/components/inplace-edit/directives/field-display/display-spent-time/' +
'display-spent-time.directive.html',
controller: InplaceDisplaySpentTimeController,
controllerAs: 'customEditorController'
};
}
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.
// ++
module.exports = function() {
angular
.module('openproject.inplace-edit')
.directive('inplaceDisplayVersion', inplaceDisplayVersion);
function inplaceDisplayVersion() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: false,
templateUrl: '/templates/work_packages/inplace_editor/main_pane.html',
controller: function($scope, $timeout) {
// controller is invoked before linker
$timeout(function() {
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',
};
templateUrl: '/components/inplace-edit/directives/field-display/display-version/' +
'display-version.directive.html',
controller: InplaceDisplayVersionController,
controllerAs: 'customEditorController'
};
}
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">
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}-start">
{{::startDateLabel}}
</label>
<input type="text"
class="inplace-edit--date-range-start-date"
id="inplace-edit--write-value--{{::field.name}}-start"
ng-change="onStartEdit()"
ng-model="startDate" ng-class="{'inplace-edit--highlight': startDateIsChanged}" />
<div class="inplace-edit--date-range-start-date-picker"></div>
<span class="delimeter">-</span>
<label
class="hidden-for-sighted"
for="inplace-edit--write-value--{{::field.name}}-end">
{{::endDateLabel}}
</label>
<input type="text"
class="inplace-edit--date-range-end-date"
id="inplace-edit--write-value--{{::field.name}}-end"
ng-change="onEndEdit()"
ng-model="endDate" ng-class="{'inplace-edit--highlight': endDateIsChanged}" />
<div class="inplace-edit--date-range-end-date-picker"></div>

@ -26,24 +26,27 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
module.exports = function(TimezoneService, ConfigurationService,
I18n, $timeout, WorkPackageFieldService,
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorDateRange', inplaceEditorDateRange);
function inplaceEditorDateRange(TimezoneService, I18n, $timeout, WorkPackageFieldService,
EditableFieldsState, Datepicker) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {},
require: '^workPackageField',
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/daterange.html',
controller: function() {
},
templateUrl: '/components/inplace-edit/directives/field-edit/edit-date-range/' +
'edit-date-range.directive.html',
controller: function() {},
controllerAs: 'customEditorController',
link: function(scope, element, attrs, fieldController) {
link: function(scope, element) {
var field = scope.field;
var customDateFormat = 'YYYY-MM-DD';
function getTitle(labelName) {
return I18n.t('js.inplace.button_edit', {
attribute: WorkPackageFieldService.getLabel(
@ -53,8 +56,13 @@ module.exports = function(TimezoneService, ConfigurationService,
});
}
scope.startDate = fieldController.writeValue.startDate;
scope.endDate = fieldController.writeValue.dueDate;
scope.startDate = field.value.startDate;
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'),
inputStart = element.find('.inplace-edit--date-range-start-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);
endDatepicker = new Datepicker(divEnd, inputEnd, scope.endDate);
startDatepicker.onChange = function(date) {
scope.startDate = fieldController.writeValue.startDate = date;
scope.startDate = field.value.startDate = date;
if (startDatepicker.prevDate.isAfter(endDatepicker.prevDate)) {
scope.startDateIsChanged = true;
scope.endDate = fieldController.writeValue.dueDate = scope.startDate;
scope.endDate = field.value.dueDate = scope.startDate;
endDatepicker.setDate(scope.endDate);
}
};
@ -90,10 +98,10 @@ module.exports = function(TimezoneService, ConfigurationService,
startDatepicker.onEdit();
};
endDatepicker.onChange = function(date) {
scope.endDate = fieldController.writeValue.dueDate = date;
scope.endDate = field.value.dueDate = date;
if (endDatepicker.prevDate.isBefore(startDatepicker.prevDate)) {
scope.endDateIsChanged = true;
scope.startDate = fieldController.writeValue.startDate = scope.endDate;
scope.startDate = field.value.startDate = scope.endDate;
startDatepicker.setDate(scope.startDate);
}
};
@ -103,11 +111,13 @@ module.exports = function(TimezoneService, ConfigurationService,
};
startDatepicker.onDone = endDatepicker.onDone = function() {
$timeout(function() {
form.scope().editPaneController.discardEditing();
});
};
$timeout(function() {
startDatepicker.focus();
EditableFieldsState.editAll.state || startDatepicker.focus();
});
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.
// ++
module.exports = function(WorkPackageFieldService, EditableFieldsState,
TimezoneService, ConfigurationService, I18n,
$timeout, Datepicker) {
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorDate', inplaceEditorDate);
function inplaceEditorDate(EditableFieldsState, TimezoneService, $timeout, Datepicker) {
var parseISODate = TimezoneService.parseISODate,
customDateFormat = 'YYYY-MM-DD',
customFormattedDate = function(date) {
return parseISODate(date).format(customDateFormat);
};
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {},
require: '^workPackageField',
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/date.html',
controller: function() {
},
templateUrl: '/components/inplace-edit/directives/field-edit/edit-date/' +
'edit-date.directive.html',
controller: function() {},
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'),
input = element.find('.inplace-edit--date'),
datepickerContainer = element.find('.inplace-edit--date-picker'),
@ -55,13 +60,11 @@ module.exports = function(WorkPackageFieldService, EditableFieldsState,
form.scope().editPaneController.submit();
};
if(scope.fieldController.writeValue) {
scope.fieldController.writeValue = customFormattedDate(scope.fieldController.writeValue);
}
field.value = field.value && customFormattedDate(field.value);
datepicker = new Datepicker(datepickerContainer, input, scope.fieldController.writeValue);
datepicker = new Datepicker(datepickerContainer, input, field.value);
datepicker.onChange = function(date) {
scope.fieldController.writeValue = date;
field.value = date;
};
scope.onEdit = function() {
datepicker.onEdit();
@ -80,7 +83,7 @@ module.exports = function(WorkPackageFieldService, EditableFieldsState,
};
$timeout(function() {
datepicker.focus();
EditableFieldsState.editAll.state || datepicker.focus();
});
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.
// ++
module.exports = function() {
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorDuration', inplaceEditorDuration);
function inplaceEditorDuration() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {},
require: '^workPackageField',
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/duration.html',
templateUrl: '/components/inplace-edit/directives/field-edit/edit-duration/' +
'edit-duration.directive.html',
controllerAs: 'customEditorController',
controller: function() {},
link: function(scope, element, attrs, fieldController) {
scope.fieldController = fieldController;
if (fieldController.writeValue === null) {
scope.customEditorController.writeValue = null;
} else {
scope.customEditorController.writeValue = Number(
moment
.duration(fieldController.writeValue)
.asHours()
.toFixed(2)
);
link: function(scope) {
var field = scope.field;
scope.numberValue = 0;
if (field.value) {
scope.numberValue = Number(moment.duration(field.value).asHours().toFixed(2));
}
scope.$watch('customEditorController.writeValue', function(value) {
if (value === null) {
fieldController.writeValue = null;
} else {
// get rounded minutes so that we don't have to send 12.223000000003
// to the server
var minutes = Number(moment.duration(value, 'hours').asMinutes().toFixed(2));
fieldController.writeValue = moment.duration(minutes, 'minutes');
// The level of indirection introduced by numberValue is necessary to prevent
// a non terminating digest cycle. The alternative would be:
// scope.$watch('field.value', function(newValue) {
// ...
// field.value = calculatedValue;
// });
// 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.
// ++
module.exports = function(EditableFieldsState) {
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorType', inplaceEditorType);
function inplaceEditorType(EditableFieldsState, FocusHelper, WorkPackageService) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {},
require: ['^inplaceEditorDisplayPane', '^workPackageField'],
templateUrl: '/templates/work_packages/inplace_editor/custom/display/spent_time.html',
controller: function() {
this.isLinkViewable = function() {
return EditableFieldsState.workPackage.links.timeEntries;
};
require: '^workPackageField',
templateUrl: '/components/inplace-edit/directives/field-edit/edit-type/' +
'edit-type.directive.html',
this.getPath = function() {
return EditableFieldsState.workPackage.links.timeEntries.href;
};
},
controllerAs: 'customEditorController',
link: function(scope, element, attrs, controllers) {
scope.displayPaneController = controllers[0];
scope.fieldController = controllers[1];
link: function(scope, element, attrs, fieldController) {
scope.$watch('field.value.props', function(newValue, oldValue) {
if (newValue.hrefTracker !== oldValue.hrefTracker) {
scope.$emit('form.updateRequired');
}
});
}
};
};
}
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.
// ++
module.exports = function(TextileService, EditableFieldsState, $sce, AutoCompleteHelper, $timeout) {
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorWikiTextarea', inplaceEditorWikiTextarea);
function inplaceEditorWikiTextarea(AutoCompleteHelper, $timeout) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {},
templateUrl: '/templates/work_packages/inplace_editor/custom/editable/wiki_textarea.html',
controller: function($scope) {
this.isPreview = false;
this.previewHtml = '';
this.autocompletePath = '/work_packages/auto_complete.json';
templateUrl: '/components/inplace-edit/directives/field-edit/edit-wiki-textarea/' +
'edit-wiki-textarea.directive.html',
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,
$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;
}));
};
},
controller: InplaceEditorWikiTextareaController,
controllerAs: 'customEditorController',
link: function(scope, element) {
scope.fieldController = scope.$parent.fieldController;
$timeout(function() {
AutoCompleteHelper.enableTextareaAutoCompletion(element.find('textarea'));
// 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
class="inplace-edit attribute-{{ fieldController.field }}"
class="inplace-edit attribute-{{ field.name }}"
ng-class="{'-busy': fieldController.state.isBusy}"
aria-busy="{{ fieldController.state.isBusy }}">
<ng-transclude></ng-transclude>

@ -26,8 +26,14 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
angular.module('openproject.api')
.factory('HALAPIResource', ['$timeout',
'$q',
require('./hal-api-resource')
]);
angular
.module('openproject.inplace-edit')
.directive('inplaceEditorMainPane', inplaceEditorMainPane);
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.
// ++
angular.module('openproject.workPackages.directives')
.directive('inplaceDisplayUser', require('./inplace-display-user-directive'))
.directive('inplaceDisplaySpentTime', require('./inplace-display-spent-time-directive'))
.directive('inplaceDisplayVersion', require('./inplace-display-version-directive'));
describe('Inplace edit service', function () {
var inplaceEdit,
resources = ['some object', 'some other object'],
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.
//++
module.exports = function(
I18n,
WORK_PACKAGE_REGULAR_EDITABLE_FIELD,
WorkPackagesHelper,
$q,
$http,
$rootScope,
$timeout,
$filter,
HookService,
NotificationsService,
EditableFieldsState
) {
angular
.module('openproject.services')
.service('WorkPackageFieldService', WorkPackageFieldService);
function WorkPackageFieldService($q, $http, $filter, I18n, WorkPackagesHelper, HookService,
EditableFieldsState ) {
function getSchema(workPackage) {
if (workPackage.form) {
@ -245,7 +238,7 @@ module.exports = function(
}
switch(fieldType) {
case 'DateRange':
inplaceType = 'daterange';
inplaceType = 'date-range';
break;
case 'Date':
inplaceType = 'date';
@ -261,7 +254,7 @@ module.exports = function(
break;
case 'Formattable':
if (workPackage.form.embedded.payload.props[field].format === 'textile') {
inplaceType = 'wiki_textarea';
inplaceType = 'wiki-textarea';
} else {
inplaceType = 'textarea';
}
@ -269,14 +262,16 @@ module.exports = function(
case 'Duration':
inplaceType = 'duration';
break;
case 'Type':
inplaceType = 'type';
break;
case 'StringObject':
case 'Version':
case 'User':
case 'Status':
case 'Priority':
case 'Category':
case 'Type':
inplaceType = 'dropdown';
case 'Version':
inplaceType = 'drop-down';
break;
}
@ -315,10 +310,10 @@ module.exports = function(
displayStrategy = 'text';
break;
case 'SpentTime':
displayStrategy = 'spent_time';
displayStrategy = 'spent-time';
break;
case 'Formattable':
displayStrategy = 'wiki_textarea';
displayStrategy = 'wiki-textarea';
break;
case 'Version':
displayStrategy = 'version';
@ -327,7 +322,7 @@ module.exports = function(
displayStrategy = 'user';
break;
case 'DateRange':
displayStrategy = 'daterange';
displayStrategy = 'date-range';
break;
case 'Date':
displayStrategy = 'date';
@ -377,7 +372,7 @@ module.exports = function(
updatedAt: 'datetime'
};
if (schema.props[field]) {
if (schema.props[field] && schema.props[field]) {
if (schema.props[field].type === 'Duration') {
var hours = moment.duration(value).asHours();
var formattedHours = $filter('number')(hours, 2);
@ -396,27 +391,6 @@ module.exports = function(
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 = {
getSchema: getSchema,
isEditable: isEditable,
@ -432,10 +406,17 @@ module.exports = function(
getAllowedValues: getAllowedValues,
format: format,
getInplaceEditStrategy: getInplaceEditStrategy,
getInplaceDisplayStrategy: getInplaceDisplayStrategy,
defaultPlaceholder: '-',
submitWorkPackageChanges: submitWorkPackageChanges
getInplaceDisplayStrategy: getInplaceDisplayStrategy
};
return WorkPackageFieldService;
};
}
WorkPackageFieldService.$inject = [
'$q',
'$http',
'$filter',
'I18n',
'WorkPackagesHelper',
'HookService',
'EditableFieldsState'];

@ -173,6 +173,9 @@ angular.module('openproject.api', []);
angular.module('openproject.templates', []);
// refactoring
angular.module('openproject.inplace-edit', []);
// main app
var openprojectApp = angular.module('openproject', [
'ui.date',
@ -192,7 +195,8 @@ var openprojectApp = angular.module('openproject', [
'cgBusy',
'openproject.api',
'openproject.templates',
'monospaced.elastic'
'monospaced.elastic',
'openproject.inplace-edit'
]);
window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') ||
@ -271,8 +275,6 @@ openprojectApp
}
]);
require('./api');
angular.module('openproject.config')
.service('ConfigurationService', [
'PathHelper',
@ -296,3 +298,6 @@ var requireTemplate = require.context('./templates', true, /\.html$/);
requireTemplate.keys().forEach(requireTemplate);
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
html-element="displayPaneController.getDynamicDirectiveName()"
work-package="displayPaneController.getWorkPackage()"
field="field"
class="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"
ng-disabled="fieldController.state.isBusy"
ng-required="fieldController.isRequired"
ng-model="fieldController.writeValue.raw"
ng-model="field.value.raw"
title="{{ fieldController.editTitle }}">
</textarea>

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

@ -27,7 +27,7 @@
</span>
<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 class="work-package-details-tab" ui-view></div>
@ -35,6 +35,6 @@
</div>
<div class="bottom-toolbar">
<work-package-details-toolbar work-package='workPackage'>
</work-package-details-toolbar>
<work-package-details-toolbar work-package='workPackage'></work-package-details-toolbar>
<work-package-edit-actions></work-package-edit-actions>
</div>

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

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

@ -17,8 +17,14 @@
</accessible-by-keyboard>
</div>
<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>
<div class="inplace-edit--write-value" ng-include="'/templates/components/inplace_editor/editable/wiki_textarea.html'" ng-click="fieldController.markActive()" tabindex="-1">
<form class="inplace-edit--form"
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 class="inplace-edit--dashboard">
<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 class="single-attribute wiki">
<work-package-field field="'description'"></work-package-field>
<work-package-field field-name="'description'"></work-package-field>
</div>
</div>
@ -46,7 +46,7 @@
ng-if="vm.isSpecified(vm.workPackage, field)"
ng-repeat-end
class="attributes-key-value--value-container">
<work-package-field field="field"></work-package-field>
<work-package-field field-name="field"></work-package-field>
</dd>
</dl>
</div>

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

@ -1,12 +1,11 @@
<div class="work-packages--details-toolbar">
<button class="button"
accesskey="3"
ng-click="editWorkPackage()">
<div class="work-packages--details-toolbar" ng-hide="editAll.state && !editAll.allowed">
<button class="button" accesskey="3" ng-click="editAll.start()" ng-disabled="!editAll.allowed">
<i class="button--icon icon-edit"></i>
<span class="button--text" ng-bind="::I18n.t('js.button_edit')"></span>
</button>
<work-package-watcher-button work-package="workPackage"
show-text="true"
disabled="editAll.state"
ng-if="displayWatchButton">
</work-package-watcher-button>
<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',
'OPERATORS_AND_LABELS_BY_FILTER_TYPE',
'NotificationsService',
'EditableFieldsState',
require('./work-packages-list-controller')
])
.factory('columnsModal', ['btfModal', function(btfModal) {

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

Loading…
Cancel
Save