Compare commits

...

107 Commits

Author SHA1 Message Date
bsatarnejad 7d4896523f show asterisks beside date in date-picker 2 years ago
Oliver Günther 262420b440
Add required validation 2 years ago
Oliver Günther e4ea29abb2
WIP 2 years ago
bsatarnejad d44e928a54 remove Non-working days only toggle 2 years ago
Oliver Günther 6f1953d140
Merge remote-tracking branch 'origin/dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Oliver Günther 30190a5a5d
Add spec to add a NWD 2 years ago
Oliver Günther a95512c55e
Use slots from single datepicker to wire up modal 2 years ago
Oliver Günther 6318017e22
Merge remote-tracking branch 'origin/feature/42358-standardise-date-pickers-2' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf c218f1c26d
Remove logs, fix wp single date form initializion 2 years ago
Benjamin Bädorf ba52f73307
Merge branch 'feature/42358-standardise-date-pickers-2' of github.com:opf/openproject into feature/42358-standardise-date-pickers-2 2 years ago
Benjamin Bädorf df49549857
Fix date modal saving for new resources 2 years ago
Benjamin Bädorf 581d1b7ea9
Fix date modal opening for new resources 2 years ago
Dombi Attila 2c885576cc Update time_logging_modal helper to find hidden field 2 years ago
Benjamin Bädorf 532f8b9779
Fix autofocus directive 2 years ago
Benjamin Bädorf 9ec00019e1
Properly focus duration when opening from the wp table 2 years ago
Benjamin Bädorf d2ab657f2c
Merge branch 'feature/42358-standardise-date-pickers-2' of github.com:opf/openproject into feature/42358-standardise-date-pickers-2 2 years ago
Benjamin Bädorf bade5147f3
Fix value update for date picker in wp table 2 years ago
Dombi Attila df74324938 Fix some new_work_package_specs 2 years ago
Benjamin Bädorf 1ad534b8e9
Fix date picker overflows in work package table, add auto alignment option for spot-drop-modal 2 years ago
Oliver Günther 4b3638a826
Merge remote-tracking branch 'origin/feature/42358-standardise-date-pickers-2' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf 8d9cf86115
Fix custom field form builder specs 2 years ago
Benjamin Bädorf a341f0b798
Fix broken specs 2 years ago
Benjamin Bädorf 09af5cf34e
Merge branch 'dev' into feature/42358-standardise-date-pickers-2 2 years ago
Benjamin Bädorf 3291a882ed
Removed unused file 2 years ago
Yule d67d18c8f5
Merge branch 'dev' into feature/42358-standardise-date-pickers-2 2 years ago
Benjamin Bädorf 1a32034950
Add date_picker form field type to FormBuilder and use it where possible 2 years ago
Benjamin Bädorf eacbc7528d
Fix overflow for cost types date picker 2 years ago
Benjamin Bädorf dc6417e14a
Add slot for trigger 2 years ago
Oliver Günther 8e928f0d97
WIP single date picker 2 years ago
Oliver Günther 46d8b0c6e0
Merge remote-tracking branch 'origin/feature/42358-standardise-date-pickers-2' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf 7401f7c749
Add slot for extra form fields in date picker, make non working days toggle configurable 2 years ago
Benjamin Bädorf f33e1b8dbd
Use op-single-date-picker for budget forms 2 years ago
Oliver Günther 55613c663d
Merge remote-tracking branch 'origin/feature/42358-standardise-date-pickers-2' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf c3d0522b97
Use op-single-date-picker for cost report filters 2 years ago
Benjamin Bädorf 3d9080085b
Remove unused old datepicker code 2 years ago
Benjamin Bädorf 510d389618
Use op-single-date-picker for backlogs, fix value from dataset loading for op-single-date-picker 2 years ago
Benjamin Bädorf 7eb7fcaf61
Use op-single-date-picker for project filters 2 years ago
Benjamin Bädorf d045a8383c
Use op-single-date-picker for version forms 2 years ago
Benjamin Bädorf 290d79aae4
Fix announcement form date picker initial value 2 years ago
Benjamin Bädorf e434ef5b0a
Use op-single-date-picker for announcement form 2 years ago
Benjamin Bädorf 3c853958d2
Use op-single-date-picker for work package bulk edit form 2 years ago
Benjamin Bädorf 9f251db19e
Use op-single-date-picker for work package move form 2 years ago
Benjamin Bädorf 0d4e3a1e2c
Have working date filters 2 years ago
Benjamin Bädorf 47c6ea330c
Use new date picker for dynamic forms 2 years ago
Benjamin Bädorf 47eac9bfd6
Use new date pickers in meetings, unit costs, and email remidners 2 years ago
Benjamin Bädorf a066f83350
Single date picker works in time entry modal 2 years ago
Dombi Attila eb78edfba1 fixup! Allow dangling underscore for _destroy 2 years ago
Benjamin Bädorf 7c346b2e82
Change detection problems 2 years ago
Dombi Attila 51f052f990 Allow dangling underscore for _destroy 2 years ago
Dombi Attila 6157248974 Add requests spec for deleting non working days 2 years ago
Benjamin Bädorf f0d86c1cc2
Separate changeset and non-changeset datepickers 2 years ago
bsatarnejad bdd0c91ca0 add a method to check if a date is a NWD 2 years ago
bsatarnejad 2b59d8fd74 add hidden inputs for a new nwd in ngoninit and add it to the calendar itself at the same time, when it is back from a failed proces 2 years ago
Benjamin Bädorf 6e41289573
Single date picker input field works 2 years ago
Benjamin Bädorf d2ca6969d2
Multi date picker opens correctly 2 years ago
bsatarnejad ff8e61090b add non-working days as test 2 years ago
Dombi Attila 53941c928b Limit json attributes 2 years ago
Dombi Attila 4b5b5a3c13 Fix specs 2 years ago
Benjamin Bädorf 3cc9c05748
Start transforming multi date modal 2 years ago
bsatarnejad 9ab429fa2e Merge branch '45001-component-to-show-the-list-of-non-working-days-of-year' of github.com:opf/openproject into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
bsatarnejad 8ee8792fa9 create hidden inputs only for added and deleted NWDs 2 years ago
Dombi Attila bcadb62f9d Update the modified_non_working_days variable to also propagate the _destroy field to the frontend 2 years ago
bsatarnejad 6b28d037dd add inputs to the form itself instead of row of calendar, while adding a new NWD 2 years ago
Dombi Attila 3c66eca1b9 Pass this.modifiedNonWorkingDays to the angular component 2 years ago
Dombi Attila 5df6dfde50 fixup! Pass ruby variable to the op-non-working-days-list component 2 years ago
Dombi Attila d0b104e501 Pass ruby variable to the op-non-working-days-list component 2 years ago
Dombi Attila 4f5f8ca03b Fix non working days attributes _deleted to _destroy 2 years ago
Dombi Attila 5aa8d0c2a1 Fix parameter name on WorkingDaysSettingsController 2 years ago
Benjamin Bädorf 050aa94c4d
Working build with a workaround for the date modal scheduling and relations services 2 years ago
Benjamin Bädorf 9a923edc36
In-between commit 2 years ago
bsatarnejad 3fd11042d9 use id returned from the API 2 years ago
bsatarnejad 20cdae4003 Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf 3fc36bda26
Fix flatpickr initialization 2 years ago
bsatarnejad a415c9b4fb set suto to content height of calendar 2 years ago
bsatarnejad 67f49f38d0 fetch data from non-working days API endpoint 2 years ago
bsatarnejad 7840a9820d Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf eb165e37b7
Merge branch 'dev' into feature/42358-standardise-date-pickers 2 years ago
bsatarnejad 5980033e8b Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
bsatarnejad edf5db21c4 reinstall list in fullcalendar 2 years ago
bsatarnejad d4ca32c14a merge branch dev into current branch 2 years ago
Benjamin Bädorf 53aac04610
Merge branch 'dev' into feature/42358-standardise-date-pickers 2 years ago
bsatarnejad bb7b27e1ac make a connection between rails and calendar list component 2 years ago
bsatarnejad 53df3054ac Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
bsatarnejad 3d14c66b00 change a warning string 2 years ago
bsatarnejad 073e335cdc format removed dates to be shown on the confirmation modal 2 years ago
bsatarnejad 18b73ae414 open confirmation modal in list of non-working days component while submitting the form 2 years ago
bsatarnejad 88acdec6bb change confirm modal to show a list of data and a warning 2 years ago
Benjamin Bädorf 8d5ec86b1c
Fix closing of single date picker 2 years ago
bsatarnejad e9aec723f3 Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf 9b63226795
Merge branch 'dev' into feature/42358-standardise-date-pickers 2 years ago
bsatarnejad 557b3c4525 fix full calendar/list css import issue in anguar 14 2 years ago
bsatarnejad fd8e3d0cde Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf c8bfaf98c1
Start rewriting op-single-date-picker from scratch 2 years ago
bsatarnejad 8aa7ec0250 Merge branch dev into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf 0cadc6cdad
Refactor date filter templates, try to reach fixed build 2 years ago
Benjamin Bädorf 764ffe28b5
Hard remove op-date-picker directory 2 years ago
bsatarnejad d5d8ee6802 Change componenet name and add empty state handleing and add button for opening the date picker modal 2 years ago
bsatarnejad 75aea8a20f Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
Benjamin Bädorf 2ae17f10fb
Remove date-picker-control, use op-single-date-picker directly 2 years ago
bsatarnejad 9d00f7e22f add delete icon to the list of non-working days 2 years ago
Benjamin Bädorf 6ef33e6151
Fix build 2 years ago
Benjamin Bädorf 0b4bcad061
Initial commit on date picker changes 2 years ago
bsatarnejad 6a514963ce Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
bsatarnejad e6e5028481 Merge branch 'dev' into 45001-component-to-show-the-list-of-non-working-days-of-year 2 years ago
bsatarnejad 0e81099a36 fix eslint errors 2 years ago
bsatarnejad 6317018f83 fix the method for fetching data from the database 2 years ago
bsatarnejad 71cbc45c92 create a new component for showing list of NWD 2 years ago
  1. 14
      app/controllers/admin/settings/working_days_settings_controller.rb
  2. 2
      app/helpers/application_helper.rb
  3. 10
      app/helpers/confirmation_dialog_helper.rb
  4. 15
      app/helpers/custom_fields_helper.rb
  5. 6
      app/services/settings/working_days_update_service.rb
  6. 11
      app/views/admin/settings/working_days_settings/show.html.erb
  7. 2
      app/views/announcements/edit.html.erb
  8. 16
      app/views/projects/filters/date/_between_dates.html.erb
  9. 12
      app/views/projects/filters/date/_on_date.html.erb
  10. 16
      app/views/versions/_form.html.erb
  11. 20
      app/views/work_packages/bulk/edit.html.erb
  12. 20
      app/views/work_packages/moves/new.html.erb
  13. 2
      config/locales/en.yml
  14. 14
      config/locales/js-en.yml
  15. 1
      frontend/.eslintrc.js
  16. 14
      frontend/package-lock.json
  17. 1
      frontend/package.json
  18. 4
      frontend/src/app/app.module.ts
  19. 4
      frontend/src/app/core/apiv3/openproject-api-v3.module.ts
  20. 4
      frontend/src/app/core/global_search/openproject-global-search.module.ts
  21. 15
      frontend/src/app/core/setup/global-dynamic-components.const.ts
  22. 4
      frontend/src/app/core/setup/globals/global-listeners.ts
  23. 31
      frontend/src/app/core/setup/globals/global-listeners/augmented-date-picker.ts
  24. 4
      frontend/src/app/features/admin/openproject-admin.module.ts
  25. 14
      frontend/src/app/features/backlogs/backlogs-page/styles/master_backlog.sass
  26. 4
      frontend/src/app/features/bim/bcf/openproject-bcf.module.ts
  27. 4
      frontend/src/app/features/bim/ifc_models/openproject-ifc-models.module.ts
  28. 4
      frontend/src/app/features/boards/openproject-boards.module.ts
  29. 4
      frontend/src/app/features/calendar/openproject-calendar.module.ts
  30. 4
      frontend/src/app/features/dashboards/openproject-dashboards.module.ts
  31. 4
      frontend/src/app/features/enterprise/openproject-enterprise.module.ts
  32. 4
      frontend/src/app/features/in-app-notifications/in-app-notifications.module.ts
  33. 4
      frontend/src/app/features/invite-user-modal/invite-user-modal.module.ts
  34. 4
      frontend/src/app/features/job-status/openproject-job-status.module.ts
  35. 3
      frontend/src/app/features/my-page/my-page.component.ts
  36. 4
      frontend/src/app/features/my-page/openproject-my-page.module.ts
  37. 4
      frontend/src/app/features/overview/openproject-overview.module.ts
  38. 4
      frontend/src/app/features/projects/openproject-projects.module.ts
  39. 4
      frontend/src/app/features/team-planner/team-planner/team-planner.module.ts
  40. 12
      frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.html
  41. 4
      frontend/src/app/features/user-preferences/user-preferences.module.ts
  42. 36
      frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.html
  43. 12
      frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts
  44. 40
      frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.html
  45. 35
      frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.ts
  46. 20
      frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.html
  47. 6
      frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.ts
  48. 29
      frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.html
  49. 25
      frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.ts
  50. 33
      frontend/src/app/features/work-packages/components/wp-activity/activity-entry.component.html
  51. 7
      frontend/src/app/features/work-packages/components/wp-activity/activity-entry.component.ts
  52. 4
      frontend/src/app/features/work-packages/components/wp-fast-table/builders/cell-builder.ts
  53. 199
      frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.html
  54. 8
      frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts
  55. 165
      frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.html
  56. 14
      frontend/src/app/features/work-packages/openproject-work-packages.module.ts
  57. 4
      frontend/src/app/shared/components/autocompleter/members-autocompleter/members.module.ts
  58. 10
      frontend/src/app/shared/components/datepicker/banner/datepicker-banner.component.ts
  59. 1
      frontend/src/app/shared/components/datepicker/constants.ts
  60. 58
      frontend/src/app/shared/components/datepicker/datepicker.ts
  61. 3
      frontend/src/app/shared/components/datepicker/helpers/date-modal.helpers.ts
  62. 113
      frontend/src/app/shared/components/datepicker/multi-date-picker/multi-date-picker.component.html
  63. 447
      frontend/src/app/shared/components/datepicker/multi-date-picker/multi-date-picker.component.ts
  64. 4
      frontend/src/app/shared/components/datepicker/scheduling-mode/datepicker-scheduling-toggle.component.ts
  65. 20
      frontend/src/app/shared/components/datepicker/services/date-modal-relations.service.ts
  66. 13
      frontend/src/app/shared/components/datepicker/services/date-modal-scheduling.service.ts
  67. 74
      frontend/src/app/shared/components/datepicker/single-date-modal/single-date.modal.html
  68. 94
      frontend/src/app/shared/components/datepicker/single-date-picker/single-date-picker.component.html
  69. 253
      frontend/src/app/shared/components/datepicker/single-date-picker/single-date-picker.component.ts
  70. 17
      frontend/src/app/shared/components/datepicker/styles/datepicker.modal.sass
  71. 4
      frontend/src/app/shared/components/datepicker/toggle/datepicker-working-days-toggle.component.ts
  72. 15
      frontend/src/app/shared/components/datepicker/wp-multi-date-form/wp-multi-date-form.component.html
  73. 183
      frontend/src/app/shared/components/datepicker/wp-multi-date-form/wp-multi-date-form.component.ts
  74. 70
      frontend/src/app/shared/components/datepicker/wp-single-date-form/wp-single-date-form.component.html
  75. 168
      frontend/src/app/shared/components/datepicker/wp-single-date-form/wp-single-date-form.component.ts
  76. 81
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-adapter/date-picker-adapter.component.ts
  77. 91
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-control/date-picker-control.component.ts
  78. 18
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-control/date-picker-control.module.ts
  79. 7
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/date-input.component.html
  80. 8
      frontend/src/app/shared/components/dynamic-forms/dynamic-forms.module.ts
  81. 2
      frontend/src/app/shared/components/fields/edit/edit-field.initializer.ts
  82. 4
      frontend/src/app/shared/components/fields/edit/edit-field.service.ts
  83. 4
      frontend/src/app/shared/components/fields/edit/field-controls/edit-field-controls.module.ts
  84. 32
      frontend/src/app/shared/components/fields/edit/field-types/combined-date-edit-field.component.html
  85. 39
      frontend/src/app/shared/components/fields/edit/field-types/combined-date-edit-field.component.ts
  86. 36
      frontend/src/app/shared/components/fields/edit/field-types/date-edit-field/date-edit-field.component.ts
  87. 11
      frontend/src/app/shared/components/fields/edit/field-types/date-edit-field/date-edit-field.module.ts
  88. 37
      frontend/src/app/shared/components/fields/edit/field-types/date-picker-edit-field.component.ts
  89. 24
      frontend/src/app/shared/components/fields/edit/field-types/days-duration-edit-field.component.html
  90. 30
      frontend/src/app/shared/components/fields/edit/field-types/days-duration-edit-field.component.ts
  91. 22
      frontend/src/app/shared/components/fields/edit/field-types/text-edit-field.component.html
  92. 36
      frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.html
  93. 6
      frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts
  94. 10
      frontend/src/app/shared/components/fields/openproject-fields.module.ts
  95. 4
      frontend/src/app/shared/components/grids/openproject-grids.module.ts
  96. 7
      frontend/src/app/shared/components/grids/widgets/project-details/project-details.component.html
  97. 1
      frontend/src/app/shared/components/header-project-select/header-project-select.component.html
  98. 33
      frontend/src/app/shared/components/modals/confirm-dialog/confirm-dialog.modal.html
  99. 3
      frontend/src/app/shared/components/modals/confirm-dialog/confirm-dialog.modal.sass
  100. 12
      frontend/src/app/shared/components/modals/confirm-dialog/confirm-dialog.modal.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -13,7 +13,7 @@ module Admin::Settings
end
def failure_callback(call)
@modified_non_working_days = call.result
@modified_non_working_days = modified_non_working_days_for(call.result)
flash[:error] = call.message || I18n.t(:notice_internal_server_error)
render action: 'show', tab: params[:tab]
end
@ -38,8 +38,18 @@ module Admin::Settings
end
def non_working_days_params
non_working_days = params[:settings].to_unsafe_hash[:non_working_days] || {}
non_working_days = params[:settings].to_unsafe_hash[:non_working_days_attributes] || {}
non_working_days.to_h.values
end
def modified_non_working_days_for(result)
return if result.nil?
result.map do |record|
json_attributes = record.as_json(only: %i[id name date])
json_attributes["_destroy"] = true if record.marked_for_destruction?
json_attributes
end
end
end
end

@ -385,7 +385,7 @@ module ApplicationHelper
end
def calendar_for(*_args)
ActiveSupport::Deprecation.warn "calendar_for has been removed. Please add the class '-augmented-datepicker' instead.", caller
ActiveSupport::Deprecation.warn "calendar_for has been removed. Please use the op-single-date-picker angular component instead", caller
end
def locale_first_day_of_week

@ -34,10 +34,14 @@ module ConfirmationDialogHelper
title: nil,
text: nil,
danger_zone: false,
show_list_data: false,
list_title: nil,
warning_text: nil,
button_continue: nil,
button_cancel: nil,
icon_continue: nil,
divider: nil
divider: nil,
passed_data: nil
)
{
'augmented-confirm-dialog': {
@ -49,6 +53,10 @@ module ConfirmationDialogHelper
}.compact,
dangerHighlighting: danger_zone,
divideContent: divider,
listTitle: list_title,
warningText: warning_text,
passedData: passed_data,
showListData: show_list_data,
icon: {
continue: icon_continue
}.compact

@ -78,8 +78,13 @@ module CustomFieldsHelper
tag = case field_format.try(:edit_as)
when 'date'
styled_text_field_tag(field_name, custom_value.value, id: field_id, class: '-augmented-datepicker', size: 10,
container_class: '-slim', required: custom_field.is_required)
angular_component_tag 'op-single-date-picker',
inputs: {
required: custom_field.is_required,
value: custom_value.value,
id: field_id,
name: field_name
}
when 'text'
styled_text_area_tag(field_name, custom_value.value, id: field_id, rows: 3, container_class: '-middle',
required: custom_field.is_required)
@ -150,7 +155,11 @@ module CustomFieldsHelper
field_format = OpenProject::CustomFieldFormat.find_by_name(custom_field.field_format)
case field_format.try(:edit_as)
when 'date'
styled_text_field_tag(field_name, '', id: field_id, size: 10, class: '-augmented-datepicker')
angular_component_tag 'op-single-date-picker',
inputs: {
id: field_id,
name: field_name,
}
when 'text'
styled_text_area_tag(field_name, '', id: field_id, rows: 3, with_text_formatting: true)
when 'bool'

@ -93,7 +93,11 @@ class Settings::WorkingDaysUpdateService < Settings::UpdateService
end
def destroy_records(ids)
wrap_result NonWorkingDay.where(id: ids).destroy_all
records = NonWorkingDay.where(id: ids)
# In case the transaction fails we also mark the records for destruction,
# this way we can display them correctly on the frontend.
records.each(&:mark_for_destruction)
wrap_result records.destroy_all
end
def wrap_result(result)

@ -42,13 +42,6 @@ See COPYRIGHT and LICENSE files for more details.
<%= styled_form_tag(
admin_settings_working_days_path,
method: :patch,
data: augmented_confirmation_dialog(
title: t('working_days.change_button'),
button_continue: t('working_days.change_button'),
text: "#{t('working_days.warning')}\n#{t(:text_are_you_sure_continue)}",
danger_zone: true,
divider: true
),
class: 'op-working-days-admin-settings'
) do %>
<section class="form--section">
@ -62,5 +55,9 @@ See COPYRIGHT and LICENSE files for more details.
label: false %>
</div>
</section>
<%= angular_component_tag 'op-non-working-days-list',
data: { modified_non_working_days: @modified_non_working_days } %>
<%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %>
<% end %>

@ -16,7 +16,7 @@
with_text_formatting: true %>
</div>
<div class="form--field">
<%= f.text_field :show_until, label: t('announcements.show_until'), container_class: '-xslim', class: '-augmented-datepicker' %>
<%= f.date_picker :show_until, label: t('announcements.show_until') %>
</div>
<div class="form--field">
<%= f.check_box :active, label: t(:label_active) %>

@ -1,6 +1,18 @@
<div class="between-dates">
<span><%= t(:label_date_from) %>:</span>
<%= text_field_tag :from_value, from_value, id: "between-dates-from-value-#{filter.name}", class: 'advanced-filters--text-field -augmented-datepicker -slim', size: '10' %>
<%= angular_component_tag 'op-single-date-picker',
inputs: {
value: from_value,
id: "between-dates-from-value-#{filter.name}",
name: "from_value"
}
%>
<span><%= t(:label_date_to) %>:</span>
<%= text_field_tag :to_value, to_value, id: "between-dates-to-value-#{filter.name}", class: 'advanced-filters--text-field -augmented-datepicker -slim', size: '10' %>
<%= angular_component_tag 'op-single-date-picker',
inputs: {
value: to_value,
id: "between-dates-to-value-#{filter.name}",
name: "to_value"
}
%>
</div>

@ -1,7 +1,11 @@
<div class="on-date">
<div class="form--field-container">
<div class="form--text-field-container -slim">
<%= text_field_tag :value, value, id: "on-date-value-#{filter.name}", class: 'advanced-filters--text-field -slim -augmented-datepicker', size: '10' %>
</div>
<div class="form--field-container -visible-overflow">
<%= angular_component_tag 'op-single-date-picker',
inputs: {
value: value,
id: "on-date-value-#{filter.name}",
name: "value"
}
%>
</div>
</div>

@ -58,11 +58,23 @@ See COPYRIGHT and LICENSE files for more details.
</div>
<div class="form--field">
<%= f.text_field :start_date, container_class: '-xslim', class: '-augmented-datepicker' %>
<label
class="form--label"
for="version_start_date"
><%= t(:start_date) %></label>
<div class="form--field-container -visible-overflow">
<%= f.date_picker :start_date %>
</div>
</div>
<div class="form--field">
<%= f.text_field :effective_date, container_class: '-xslim', class: '-augmented-datepicker' %>
<label
class="form--label"
for="version_effective_date"
><%= t(:effective_date) %></label>
<div class="form--field-container -visible-overflow">
<%= f.date_picker :effective_date %>
</div>
</div>
<div class="form--field">

@ -48,7 +48,7 @@ See COPYRIGHT and LICENSE files for more details.
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= t(:label_change_properties) %></legend>
<div class="grid-block">
<div class="grid-block grid-block_visible-overflow">
<div class="grid-content medium-6">
<div class="form--field">
<%= styled_label_tag :work_package_type_id, WorkPackage.human_attribute_name(:type) %>
@ -150,14 +150,24 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<div class="form--field">
<%= styled_label_tag :work_package_start_date, WorkPackage.human_attribute_name(:start_date) %>
<div class="form--field-container">
<%= styled_text_field_tag 'work_package[start_date]', '', size: 10, class: '-augmented-datepicker' %>
<div class="form--field-container -visible-overflow">
<%= angular_component_tag 'op-single-date-picker',
inputs: {
id: "work_package_start_date",
name: "work_package[start_date]"
}
%>
</div>
</div>
<div class="form--field">
<%= styled_label_tag :work_package_due_date, WorkPackage.human_attribute_name(:due_date) %>
<div class="form--field-container">
<%= styled_text_field_tag 'work_package[due_date]', '', size: 10, class: '-augmented-datepicker' %>
<div class="form--field-container -visible-overflow">
<%= angular_component_tag 'op-single-date-picker',
inputs: {
id: "work_package_due_date",
name: "work_package[due_date]"
}
%>
</div>
</div>
<% if WorkPackage.use_field_for_done_ratio? %>

@ -53,7 +53,7 @@ See COPYRIGHT and LICENSE files for more details.
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= t(:label_change_properties) %></legend>
<div class="grid-block">
<div class="grid-block grid-block_visible-overflow">
<div class="grid-content medium-6">
<div class="form--field">
<label class="form--label" for="new_project_id"><%= WorkPackage.human_attribute_name(:project) %>:</label>
@ -133,14 +133,24 @@ See COPYRIGHT and LICENSE files for more details.
<div class="grid-content medium-6">
<div class="form--field">
<label class="form--label" for='start_date'><%= WorkPackage.human_attribute_name(:start_date) %></label>
<div class="form--field-container">
<%= styled_text_field_tag 'start_date', '', size: 10, class: '-augmented-datepicker' %>
<div class="form--field-container -visible-overflow">
<%= angular_component_tag 'op-single-date-picker',
inputs: {
id: "start_date",
name: "start_date"
}
%>
</div>
</div>
<div class="form--field">
<label class="form--label" for='due_date'><%= WorkPackage.human_attribute_name(:due_date) %></label>
<div class="form--field-container">
<%= styled_text_field_tag 'due_date', '', size: 10, class: '-augmented-datepicker' %>
<div class="form--field-container -visible-overflow">
<%= angular_component_tag 'op-single-date-picker',
inputs: {
id: "due_date",
name: "due_date"
}
%>
</div>
</div>
<% if @target_type %>

@ -3154,7 +3154,7 @@ en:
Non-working days are skipped when scheduling work packages and are excluded when calculating duration. This can be overridden at a work-package level.
change_button: "Change working days"
warning: >
Changing which days of the week are considered working days can affect the start and finish days of all work packages in all projects in this instance.
Changing which days of the week are considered working days or non-working days can affect the start and finish days of all work packages in all projects in this instance. Please not that changes are only applied after you click on the save changes button.
journal_note:
changed: _**Working days** changed (%{changes})._
days:

@ -278,6 +278,20 @@ en:
unlimited: "Unlimited"
you_contribute: "Developers need to pay their bills, too. By upgrading to the Enterprise edition, you will be supporting this open source community effort and contributing to its development, maintenance and continuous improvement."
working_days:
calendar:
empty_state_header: "Non-working days"
empty_state_description: 'No specific non-working days are defined for this year. Click on "Add non-working day" below to add a date.'
new_date: '(new)'
add_non_working_day: "Add non-working day"
change_button: "Save and reschedule"
change_title: "Change working days"
removed_title: "You will remove the following days from the non-working days list:"
change_description: "Changing which days of the week are considered working days or non-working days can affect the start and finish days of all work packages in all projects in this instance."
warning: >
The changes might take some time to take effect. You will be notified when all relevant work packages have been updated.
Are you sure you want to to continue?
custom_actions:
date:
specific: 'on'

@ -143,6 +143,7 @@ module.exports = {
"_embedded",
"_meta",
"_type",
"_destroy",
],
allowAfterThis: true,
allowAfterSuper: false,

@ -29,6 +29,7 @@
"@fullcalendar/core": "^6.0.2",
"@fullcalendar/daygrid": "^6.0.2",
"@fullcalendar/interaction": "^6.0.2",
"@fullcalendar/list": "^6.0.2",
"@fullcalendar/resource": "^6.0.2",
"@fullcalendar/resource-common": "^6.0.0-beta.1",
"@fullcalendar/resource-timeline": "^6.0.2",
@ -4870,6 +4871,14 @@
"@fullcalendar/core": "~6.0.2"
}
},
"node_modules/@fullcalendar/list": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.0.2.tgz",
"integrity": "sha512-xeTNqGgQvW6u2isX8P3IV8w19Jzn/4Fs6nNc5tiEs0WJ6zd1bYEfruzDL0WPMMmcydgpkv0YHTLMwlnlFeq9+g==",
"peerDependencies": {
"@fullcalendar/core": "~6.0.2"
}
},
"node_modules/@fullcalendar/premium-common": {
"version": "6.0.0-beta.1",
"resolved": "https://registry.npmjs.org/@fullcalendar/premium-common/-/premium-common-6.0.0-beta.1.tgz",
@ -51897,6 +51906,11 @@
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.0.2.tgz",
"integrity": "sha512-RPWWCp6wdzVCc3XdcBUZuduFGX/mB8OrV2xBF5wrGFbGAZE3OYuEPcVnLF1SsC+pJVntG6z1fL5aQ/OM8nul9Q=="
},
"@fullcalendar/list": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.0.2.tgz",
"integrity": "sha512-xeTNqGgQvW6u2isX8P3IV8w19Jzn/4Fs6nNc5tiEs0WJ6zd1bYEfruzDL0WPMMmcydgpkv0YHTLMwlnlFeq9+g=="
},
"@fullcalendar/premium-common": {
"version": "6.0.0-beta.1",
"resolved": "https://registry.npmjs.org/@fullcalendar/premium-common/-/premium-common-6.0.0-beta.1.tgz",

@ -103,6 +103,7 @@
"@fullcalendar/core": "^6.0.2",
"@fullcalendar/daygrid": "^6.0.2",
"@fullcalendar/interaction": "^6.0.2",
"@fullcalendar/list": "^6.0.2",
"@fullcalendar/resource": "^6.0.2",
"@fullcalendar/resource-common": "^6.0.0-beta.1",
"@fullcalendar/resource-timeline": "^6.0.2",

@ -40,7 +40,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { OpContextMenuTrigger } from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive';
import { States } from 'core-app/core/states/states.service';
import { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openproject-fields.module';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpSpotModule } from 'core-app/spot/spot.module';
import { OpDragScrollDirective } from 'core-app/shared/directives/op-drag-scroll/op-drag-scroll.directive';
import { DynamicBootstrapper } from 'core-app/core/setup/globals/dynamic-bootstrapper';
@ -121,7 +121,7 @@ export function initializeServices(injector:Injector) {
A11yModule,
// Commons
OPSharedModule,
OpSharedModule,
// Design System
OpSpotModule,
// State module

@ -26,13 +26,13 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { NgModule } from '@angular/core';
import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.module';
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectHalModule,
],
})

@ -35,11 +35,11 @@ import { GlobalSearchTitleComponent } from 'core-app/core/global_search/title/gl
import { GlobalSearchService } from 'core-app/core/global_search/services/global-search.service';
import { GlobalSearchWorkPackagesEntryComponent } from 'core-app/core/global_search/global-search-work-packages-entry.component';
import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autocompleter/openproject-autocompleter.module';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectWorkPackagesModule,
OpenprojectAutocompleterModule,
],

@ -162,6 +162,10 @@ import {
EnterprisePageComponent,
enterprisePageSelector,
} from 'core-app/shared/components/enterprise-page/enterprise-page.component';
import {
OpNonWorkingDaysListComponent,
nonWorkingDaysListSelector,
} from 'core-app/shared/components/op-non-working-days-list/op-non-working-days-list.component';
import {
EEActiveSavedTrialComponent,
enterpriseActiveSavedTrialSelector,
@ -203,7 +207,14 @@ import {
CalendarSidemenuComponent,
opCalendarSidemenuSelector,
} from 'core-app/features/calendar/sidemenu/calendar-sidemenu.component';
import { OpModalOverlayComponent, opModalOverlaySelector } from 'core-app/shared/components/modal/modal-overlay.component';
import {
OpModalOverlayComponent,
opModalOverlaySelector,
} from 'core-app/shared/components/modal/modal-overlay.component';
import {
OpSingleDatePickerComponent,
opSingleDatePickerSelector,
} from 'core-app/shared/components/datepicker/single-date-picker/single-date-picker.component';
export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: appBaseSelector, cls: ApplicationBaseComponent },
@ -238,6 +249,7 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: globalSearchSelector, cls: GlobalSearchInputComponent },
{ selector: collapsibleSectionAugmentSelector, cls: CollapsibleSectionComponent },
{ selector: enterpriseBannerSelector, cls: EnterpriseBannerComponent },
{ selector: nonWorkingDaysListSelector, cls: OpNonWorkingDaysListComponent },
{ selector: enterprisePageSelector, cls: EnterprisePageComponent },
{ selector: noResultsSelector, cls: NoResultsComponent },
{ selector: enterpriseBaseSelector, cls: EnterpriseBaseComponent },
@ -261,4 +273,5 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: opInAppNotificationBellSelector, cls: InAppNotificationBellComponent },
{ selector: ianMenuSelector, cls: IanMenuComponent },
{ selector: opModalOverlaySelector, cls: OpModalOverlayComponent },
{ selector: opSingleDatePickerSelector, cls: OpSingleDatePickerComponent },
];

@ -37,7 +37,6 @@ import { dangerZoneValidation } from 'core-app/core/setup/globals/global-listene
import { setupServerResponse } from 'core-app/core/setup/globals/global-listeners/setup-server-response';
import { listenToSettingChanges } from 'core-app/core/setup/globals/global-listeners/settings';
import { detectOnboardingTour } from 'core-app/core/setup/globals/onboarding/onboarding_tour_trigger';
import { augmentedDatePicker } from './global-listeners/augmented-date-picker';
import { performAnchorHijacking } from './global-listeners/link-hijacking';
/**
@ -48,9 +47,6 @@ export function initializeGlobalListeners():void {
.on('click', (evt:any) => {
const target = jQuery(evt.target) as JQuery;
// Create datepickers dynamically for Rails-based views
augmentedDatePicker(evt, target);
// Prevent angular handling clicks on href="#..." links from other libraries
// (especially jquery-ui and its datepicker) from routing to <base url>/#
performAnchorHijacking(evt, target);

@ -1,31 +0,0 @@
import { DatePicker } from 'core-app/shared/components/op-date-picker/datepicker';
/**
* Our application is still a hybrid one, meaning most routes are still
* handled by Rails. As such, we disable the default link-hijacking that
* Angular's HTML5-mode with <base href="/"> results in
* @param evt
* @param target
*/
export function augmentedDatePicker(evt:JQuery.TriggeredEvent, target:JQuery) {
if (target.hasClass('-augmented-datepicker')) {
target
.attr('autocomplete', 'off'); // Disable autocomplete for those fields
window.OpenProject.getPluginContext()
.then((context) => {
const datePicker = new DatePicker(
context.injector,
'.-augmented-datepicker',
target.val() as string,
{
weekNumbers: true,
allowInput: true,
},
target[0],
);
datePicker.show();
})
.catch(() => {});
}
}

@ -27,7 +27,7 @@
//++
import { NgModule } from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { DragulaModule } from 'ng2-dragula';
import { TypeFormAttributeGroupComponent } from 'core-app/features/admin/types/attribute-group.component';
import { TypeFormConfigurationComponent } from 'core-app/features/admin/types/type-form-configuration.component';
@ -38,7 +38,7 @@ import { EditableQueryPropsComponent } from 'core-app/features/admin/editable-qu
@NgModule({
imports: [
DragulaModule.forRoot(),
OPSharedModule,
OpSharedModule,
],
providers: [
],

@ -283,6 +283,11 @@
.velocity, .add_new_story
display: none
.backlog .sprint.editing
.editors
display: flex
align-items: center
flex-direction: row-reverse
.editor
font-size: 0.9rem
line-height: 1.5rem
@ -291,10 +296,11 @@
padding: 0
&.name
min-width: 15em
width: calc(100% - 2 * 85px - 10px)
&.start_date, &.effective_date
width: 85px
flex-basis: 15em
&.start_date,
&.effective_date
margin-left: 0.5em
flex-basis: 12.5em
.stories .story.editing
>

@ -30,7 +30,7 @@ import {
Injector,
NgModule,
} from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
import { DisplayFieldService } from 'core-app/shared/components/fields/display/display-field.service';
import { BcfThumbnailDisplayField } from 'core-app/features/bim/bcf/fields/display/bcf-thumbnail-field.module';
@ -64,7 +64,7 @@ export const viewerBridgeServiceFactory = (injector:Injector) => {
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
NgxGalleryModule,
],
providers: [

@ -28,7 +28,7 @@
import { NgModule } from '@angular/core';
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { UIRouterModule } from '@uirouter/angular';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { IFC_ROUTES } from 'core-app/features/bim/ifc_models/openproject-ifc-models.routes';
import { IFCViewerPageComponent } from 'core-app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component';
import { BcfViewToggleButtonComponent } from 'core-app/features/bim/ifc_models/toolbar/view-toggle/bcf-view-toggle-button.component';
@ -46,7 +46,7 @@ import { BcfSplitRightComponent } from 'core-app/features/bim/ifc_models/bcf/spl
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectFieldsModule,
OpenprojectHalModule,
OpenprojectBcfModule,

@ -27,7 +27,7 @@
//++
import { NgModule } from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { UIRouterModule } from '@uirouter/angular';
@ -60,7 +60,7 @@ import { TileViewComponent } from './tile-view/tile-view.component';
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectWorkPackagesModule,
OpenprojectModalModule,
DragScrollModule,

@ -26,7 +26,7 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { NgModule } from '@angular/core';
import { FullCalendarModule } from '@fullcalendar/angular';
import { WorkPackagesCalendarComponent } from 'core-app/features/calendar/wp-calendar/wp-calendar.component';
@ -42,7 +42,7 @@ import { CalendarSidemenuComponent } from './sidemenu/calendar-sidemenu.componen
@NgModule({
imports: [
// Commons
OPSharedModule,
OpSharedModule,
// Routes for /calendar
UIRouterModule.forChild({ states: CALENDAR_ROUTES }),

@ -27,7 +27,7 @@
//++
import { NgModule } from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { Ng2StateDeclaration, UIRouter, UIRouterModule } from '@uirouter/angular';
import { DashboardComponent } from 'core-app/features/dashboards/dashboard/dashboard.component';
import { OpenprojectGridsModule } from 'core-app/shared/components/grids/openproject-grids.module';
@ -61,7 +61,7 @@ export function uiRouterDashboardsConfiguration(uiRouter:UIRouter) {
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectGridsModule,

@ -27,7 +27,7 @@
//++
import { NgModule } from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { EnterpriseTrialService } from 'core-app/features/enterprise/enterprise-trial.service';
import { EnterpriseBaseComponent } from 'core-app/features/enterprise/enterprise-base.component';
@ -40,7 +40,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectModalModule,
FormsModule,
ReactiveFormsModule,

@ -1,5 +1,5 @@
import { NgModule } from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { CommonModule } from '@angular/common';
import { IconModule } from 'core-app/shared/components/icon/icon.module';
import { InAppNotificationBellComponent } from 'core-app/features/in-app-notifications/bell/in-app-notification-bell.component';
@ -41,7 +41,7 @@ import { InAppNotificationsDateAlertsUpsaleComponent } from 'core-app/features/i
InAppNotificationsDateAlertsUpsaleComponent,
],
imports: [
OPSharedModule,
OpSharedModule,
// Routes for /backlogs
UIRouterModule.forChild({
states: IAN_ROUTES,

@ -8,7 +8,7 @@ import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.m
import { InviteUserButtonModule } from 'core-app/features/invite-user-modal/button/invite-user-button.module';
import { DynamicFormsModule } from 'core-app/shared/components/dynamic-forms/dynamic-forms.module';
import { OpInviteUserModalAugmentService } from 'core-app/features/invite-user-modal/invite-user-modal-augment.service';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpInviteUserModalService } from 'core-app/features/invite-user-modal/invite-user-modal.service';
import { InviteUserModalComponent } from './invite-user.component';
import { ProjectSelectionComponent } from './project-selection/project-selection.component';
@ -28,7 +28,7 @@ export function initializeServices(injector:Injector) {
@NgModule({
imports: [
CommonModule,
OPSharedModule,
OpSharedModule,
OpenprojectModalModule,
NgSelectModule,
ReactiveFormsModule,

@ -27,7 +27,7 @@
//++
import { NgModule } from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { Ng2StateDeclaration, UIRouterModule } from '@uirouter/angular';
import { DisplayJobPageComponent } from 'core-app/features/job-status/display-job-page/display-job-page.component';
@ -48,7 +48,7 @@ export const JOB_STATUS_ROUTE:Ng2StateDeclaration[] = [
@NgModule({
imports: [
// Commons
OPSharedModule,
OpSharedModule,
OpenprojectModalModule,
// Routes for /job_statuses/:uuid

@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, ViewEncapsulation } from '@angular/core';
import { GRID_PROVIDERS } from 'core-app/shared/components/grids/grid/grid.component';
import { GridPageComponent } from 'core-app/shared/components/grids/grid/page/grid-page.component';
@ -6,6 +6,7 @@ import { GridPageComponent } from 'core-app/shared/components/grids/grid/page/gr
templateUrl: '../../shared/components/grids/grid/page/grid-page.component.html',
styleUrls: ['../../shared/components/grids/grid/page/grid-page.component.sass'],
providers: GRID_PROVIDERS,
encapsulation: ViewEncapsulation.None,
})
export class MyPageComponent extends GridPageComponent {
protected i18nNamespace():string {

@ -28,7 +28,7 @@
import { NgModule } from '@angular/core';
import { Ng2StateDeclaration, UIRouterModule } from '@uirouter/angular';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { OpenprojectGridsModule } from 'core-app/shared/components/grids/openproject-grids.module';
import { MyPageComponent } from 'core-app/features/my-page/my-page.component';
@ -47,7 +47,7 @@ export const MY_PAGE_ROUTES:Ng2StateDeclaration[] = [
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectGridsModule,
OpenprojectModalModule,

@ -27,7 +27,7 @@
//++
import { NgModule } from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { Ng2StateDeclaration, UIRouter, UIRouterModule } from '@uirouter/angular';
import { OpenprojectGridsModule } from 'core-app/shared/components/grids/openproject-grids.module';
import { OverviewComponent } from 'core-app/features/overview/overview.component';
@ -60,7 +60,7 @@ export function uiRouterOverviewConfiguration(uiRouter:UIRouter):void {
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
OpenprojectGridsModule,

@ -34,14 +34,14 @@ import { PROJECTS_ROUTES, uiRouterProjectsConfiguration } from 'core-app/feature
import { DynamicFormsModule } from 'core-app/shared/components/dynamic-forms/dynamic-forms.module';
import { NewProjectComponent } from 'core-app/features/projects/components/new-project/new-project.component';
import { ReactiveFormsModule } from '@angular/forms';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { CopyProjectComponent } from 'core-app/features/projects/components/copy-project/copy-project.component';
import { ProjectsComponent } from './components/projects/projects.component';
@NgModule({
imports: [
// Commons
OPSharedModule,
OpSharedModule,
ReactiveFormsModule,
OpenprojectHalModule,

@ -11,7 +11,7 @@ import { TEAM_PLANNER_ROUTES } from 'core-app/features/team-planner/team-planner
import { TeamPlannerComponent } from 'core-app/features/team-planner/team-planner/planner/team-planner.component';
import { AddAssigneeComponent } from 'core-app/features/team-planner/team-planner/assignee/add-assignee.component';
import { TeamPlannerPageComponent } from 'core-app/features/team-planner/team-planner/page/team-planner-page.component';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { AddExistingPaneComponent } from './add-work-packages/add-existing-pane.component';
import { OpenprojectContentLoaderModule } from 'core-app/shared/components/op-content-loader/openproject-content-loader.module';
import { TeamPlannerSidemenuComponent } from 'core-app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component';
@ -27,7 +27,7 @@ import { TeamPlannerViewSelectMenuDirective } from 'core-app/features/team-plann
TeamPlannerViewSelectMenuDirective,
],
imports: [
OPSharedModule,
OpSharedModule,
// Routes for /team_planner
UIRouterModule.forChild({
states: TEAM_PLANNER_ROUTES,

@ -14,12 +14,10 @@
/>
</spot-selector-field>
<op-range-date-picker
[size]="23"
<op-multi-date-picker
*ngIf="(enabled$ | async)"
[required]="enabled$ | async"
[disabled]="!(enabled$ | async)"
(changed)="setDates($event)"
[initialDates]="selectedDates$ | async"
>
</op-range-date-picker>
[value]="selectedDates$ | async"
(valueChange)="setDates($event)"
></op-multi-date-picker>
</div>

@ -5,7 +5,7 @@ import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autocompleter/openproject-autocompleter.module';
import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service';
import { NotificationsSettingsPageComponent } from 'core-app/features/user-preferences/notifications-settings/page/notifications-settings-page.component';
@ -38,7 +38,7 @@ import { PauseRemindersComponent } from './reminder-settings/pause-reminders/pau
],
imports: [
CommonModule,
OPSharedModule,
OpSharedModule,
OpenprojectAutocompleterModule,
FormsModule,
ReactiveFormsModule,

@ -1,16 +1,20 @@
<div class="inline-label" id="div-values-{{filter.id}}">
<op-single-date-picker (changed)="value = isoDateParser($event)"
[initialDate]="isoDateFormatter(value)"
[opAutofocus]="shouldFocus"
required="true"
[id]="'values-' + filter.id"
[name]="'v[' + filter.id + ']'"
classes="advanced-filters--date-field"
size="10">
</op-single-date-picker>
<span class="advanced-filters--tooltip-trigger -multiline"
[attr.data-tooltip]="timeZoneText"
*ngIf="isTimeZoneDifferent">
<op-icon icon-classes="icon icon-warning"></op-icon>
</span>
</div>
<op-single-date-picker
[id]="'values-' + filter.id"
[name]="'v[' + filter.id + ']'"
required="true"
(valueChange)="value = isoDateParser($event)"
[value]="isoDateFormatter(value)"
[opAutofocus]="shouldFocus"
classes="advanced-filters--date-field"
></op-single-date-picker>
<span
class="advanced-filters--tooltip-trigger -multiline"
[attr.data-tooltip]="timeZoneText"
*ngIf="isTimeZoneDifferent"
>
<span class="spot-icon spot-icon_warning"></span>
</span>

@ -27,7 +27,11 @@
//++
import {
Component, Input, OnInit, Output,
Component,
Input,
HostBinding,
OnInit,
Output,
} from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
@ -43,6 +47,12 @@ import { AbstractDateTimeValueController } from '../abstract-filter-date-time-va
templateUrl: './filter-date-time-value.component.html',
})
export class FilterDateTimeValueComponent extends AbstractDateTimeValueController implements OnInit {
@HostBinding('id') get id() {
return `div-values-${this.filter.id}`;
}
@HostBinding('class.inline-label') className = true;
@Input() public shouldFocus = false;
@Input() public filter:QueryFilterInstanceResource;

@ -1,27 +1,17 @@
<div id="div-values-{{filter.id}}" class="inline-label">
<op-single-date-picker (changed)="begin = isoDateParser($event)"
[initialDate]="isoDateFormatter(begin)"
[opAutofocus]="shouldFocus"
required="true"
[id]="'values-' + filter.id + '-begin'"
[name]="'v[' + filter.id + ']-begin'"
classes="advanced-filters--date-field"
size="10">
</op-single-date-picker>
<op-multi-date-picker
[id]="'values-' + filter.id"
[name]="'v[' + filter.id + ']'"
<span class="advanced-filters--affix" [textContent]="text.spacer">
</span>
[(ngModel)]="value"
[opAutofocus]="shouldFocus"
<op-single-date-picker (changed)="end = isoDateParser($event)"
[initialDate]="isoDateFormatter(end)"
[id]="'values-' + filter.id + '-end'"
[name]="'v[' + filter.id + ']-end'"
classes="advanced-filters--date-field"
size="10">
</op-single-date-picker>
<span class="advanced-filters--tooltip-trigger -multiline"
*ngIf="isTimeZoneDifferent"
[attr.data-tooltip]="timeZoneText">
<op-icon icon-classes="icon icon-warning"></op-icon>
</span>
</div>
classes="advanced-filters--date-field"
></op-multi-date-picker>
<span
class="advanced-filters--tooltip-trigger -multiline"
*ngIf="isTimeZoneDifferent"
[attr.data-tooltip]="timeZoneText"
>
<span class="spot-icon spot-icon_warning"></span>
</span>

@ -29,7 +29,11 @@
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { Moment } from 'moment';
import {
Component, Input, OnInit, Output,
HostBinding,
Component,
Input,
OnInit,
Output,
} from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { DebouncedEventEmitter } from 'core-app/shared/helpers/rxjs/debounced-event-emitter';
@ -43,6 +47,12 @@ import { AbstractDateTimeValueController } from '../abstract-filter-date-time-va
templateUrl: './filter-date-times-value.component.html',
})
export class FilterDateTimesValueComponent extends AbstractDateTimeValueController implements OnInit {
@HostBinding('id') get id() {
return `div-values-${this.filter.id}`;
}
@HostBinding('class.inline-label') className = true;
@Input() public shouldFocus = false;
@Input() public filter:QueryFilterInstanceResource;
@ -53,27 +63,28 @@ export class FilterDateTimesValueComponent extends AbstractDateTimeValueControll
spacer: this.I18n.t('js.filter.value_spacer'),
};
constructor(readonly I18n:I18nService,
readonly timezoneService:TimezoneService) {
constructor(
readonly I18n:I18nService,
readonly timezoneService:TimezoneService,
) {
super(I18n, timezoneService);
}
public get begin():HalResource|string {
return this.filter.values[0];
public get value():(HalResource[]|string[]) {
return this.filter.values;
}
public set begin(val) {
this.filter.values[0] = val || '';
public set value(val:(HalResource[]|string[])) {
this.filter.values = val;
this.filterChanged.emit(this.filter);
}
public get end() {
return this.filter.values[1];
public get begin() {
return this.filter.values[0];
}
public set end(val) {
this.filter.values[1] = val || '';
this.filterChanged.emit(this.filter);
public get end() {
return this.filter.values[1];
}
public get lowerBoundary():Moment|null {

@ -1,11 +1,9 @@
<div id="div-values-{{filter.id}}">
<op-single-date-picker (changed)="value = parser($event)"
[initialDate]="formatter(value)"
[opAutofocus]="shouldFocus"
required="true"
[id]="'values-' + filter.id"
[name]="'v[' + filter.id + ']'"
classes="advanced-filters--date-field"
size="10">
</op-single-date-picker>
</div>
<op-single-date-picker
classes="advanced-filters--date-field"
(changed)="value = parser($event)"
[value]="formatter(value)"
[opAutofocus]="shouldFocus"
[required]="true"
[id]="'values-' + filter.id"
[name]="'v[' + filter.id + ']'"
></op-single-date-picker>

@ -26,7 +26,7 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { Component, Input, Output } from '@angular/core';
import { Component, HostBinding, Input, Output } from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { DebouncedEventEmitter } from 'core-app/shared/helpers/rxjs/debounced-event-emitter';
@ -40,6 +40,10 @@ import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/que
templateUrl: './filter-date-value.component.html',
})
export class FilterDateValueComponent extends UntilDestroyedMixin {
@HostBinding('id') get id() {
return `div-values-${this.filter.id}`;
}
@Input() public shouldFocus = false;
@Input() public filter:QueryFilterInstanceResource;

@ -1,24 +1,9 @@
<div id="div-values-{{filter.id}}"
class="inline-label">
<op-single-date-picker (changed)="begin = parser($event)"
[initialDate]="formatter(begin)"
[opAutofocus]="shouldFocus"
required="true"
[id]="'values-' + filter.id + '-begin'"
[name]="'v[' + filter.id + ']-begin'"
classes="advanced-filters--date-field"
size="10">
</op-single-date-picker>
<op-multi-date-picker
[id]="'values-' + filter.id"
[name]="'v[' + filter.id + ']'"
<span class="advanced-filters--affix" [textContent]="text.spacer">
</span>
[(ngModel)]="value"
[opAutofocus]="shouldFocus"
<op-single-date-picker (changed)="end = parser($event)"
[initialDate]="formatter(end)"
required="true"
[id]="'values-' + filter.id + '-end'"
[name]="'v[' + filter.id + ']-end'"
classes="advanced-filters--date-field"
size="10">
</op-single-date-picker>
</div>
classes="advanced-filters--date-field"
></op-multi-date-picker>

@ -27,7 +27,7 @@
//++
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { Component, Input, Output } from '@angular/core';
import { Component, HostBinding, Input, Output } from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { DebouncedEventEmitter } from 'core-app/shared/helpers/rxjs/debounced-event-emitter';
import * as moment from 'moment';
@ -41,6 +41,12 @@ import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/que
templateUrl: './filter-dates-value.component.html',
})
export class FilterDatesValueComponent extends UntilDestroyedMixin {
@HostBinding('id') get id() {
return `div-values-${this.filter.id}`;
}
@HostBinding('class.inline-label') className = true;
@Input() public shouldFocus = false;
@Input() public filter:QueryFilterInstanceResource;
@ -56,22 +62,21 @@ export class FilterDatesValueComponent extends UntilDestroyedMixin {
super();
}
public get begin():any {
return this.filter.values[0];
public get value():(HalResource[]|string[]) {
return this.filter.values;
}
public set begin(val:any) {
this.filter.values[0] = val || '';
public set value(val:(HalResource[]|string[])) {
this.filter.values = val;
this.filterChanged.emit(this.filter);
}
public get end():HalResource|string {
return this.filter.values[1];
public get begin():any {
return this.filter.values[0];
}
public set end(val) {
this.filter.values[1] = val || '';
this.filterChanged.emit(this.filter);
public get end():HalResource|string {
return this.filter.values[1];
}
public parser(data:any) {

@ -1,22 +1,21 @@
<div
id="activity-{{ activityNo }}"
[attr.data-qa-activity-number]="activityNo"
[ngSwitch]="activityType"
>
<div [ngSwitch]="activityType">
<revision-activity
*ngSwitchCase="'Revision'"
[workPackage]="workPackage"
[activity]="activity"
[activityNo]="activityNo"
[hasUnreadNotification]="hasUnreadNotification"
></revision-activity>
<user-activity
*ngSwitchDefault
[workPackage]="workPackage"
[activity]="activity"
[activityNo]="activityNo"
[isInitial]="isInitial"
[hasUnreadNotification]="hasUnreadNotification"
></user-activity>
</div>
<revision-activity
*ngSwitchCase="'Revision'"
[workPackage]="workPackage"
[activity]="activity"
[activityNo]="activityNo"
[hasUnreadNotification]="hasUnreadNotification"
></revision-activity>
<user-activity
*ngSwitchDefault
[workPackage]="workPackage"
[activity]="activity"
[activityNo]="activityNo"
[isInitial]="isInitial"
[hasUnreadNotification]="hasUnreadNotification"
></user-activity>
</div>

@ -51,9 +51,10 @@ export class ActivityEntryComponent implements OnInit {
public activityType:string;
constructor(readonly PathHelper:PathHelperService,
readonly I18n:I18nService) {
}
constructor(
readonly PathHelper:PathHelperService,
readonly I18n:I18nService,
) { }
ngOnInit() {
this.projectId = idFromLink(this.workPackage.project.href);

@ -28,6 +28,10 @@ export class CellBuilder {
td.classList.add('-max');
}
if ([ 'startDate', 'dueDate', 'duration' ].indexOf(attribute) !== -1) {
td.classList.add('-no-ellipsis');
}
const schema = this.schemaCache.of(workPackage).ofProperty(attribute);
if (schema && schema.type === 'User') {
td.classList.add('-contains-avatar');

@ -0,0 +1,199 @@
<div
*ngIf="workPackage"
class="work-package--single-view"
[ngClass]="{'work-package--single-view_with-columns': showTwoColumnLayout()}"
data-selector="wp-single-view"
>
<div
class="wp-new--subject-wrapper"
*ngIf="isNewResource"
>
<op-editable-attribute-field
[resource]="workPackage"
[wrapperClasses]="'-no-label'"
[fieldName]="'subject'"
></op-editable-attribute-field>
</div>
<div class="wp-info-wrapper">
<wp-status-button
*ngIf="!isNewResource"
[workPackage]="workPackage"
></wp-status-button>
<attribute-help-text
[attribute]="'status'"
[attributeScope]="'WorkPackage'"
*ngIf="!isNewResource"
></attribute-help-text>
<div
class="work-packages--info-row"
*ngIf="!isNewResource"
>
<span [textContent]="idLabel"></span>:
<span [textContent]="text.infoRow.createdBy"></span>
<!-- The space has to be in an extra span
because otherwise the browser would add a second space after it -->
<span>&nbsp;</span>
<op-user-link
class="user-link"
[user]="workPackage.author"
></op-user-link>
<span>.&nbsp;</span>
<span [textContent]="text.infoRow.lastUpdatedOn"></span>
<span>&nbsp;</span>
<op-date-time [dateTimeValue]="workPackage.updatedAt"></op-date-time>
<span>.</span>
</div>
<wp-custom-actions [workPackage]="workPackage" class="custom-actions"></wp-custom-actions>
</div>
<div
class="attributes-group -project-context __overflowing_element_container __overflowing_project_context"
*ngIf="projectContext && projectContext.field"
data-overflowing-identifier=".__overflowing_project_context"
>
<div>
<p class="wp-project-context--warning" [textContent]="text.project.required"></p>
<div
class="attributes-key-value"
[ngClass]="{'-span-all-columns': descriptor.spanAll }"
*ngFor="let descriptor of projectContext.field; trackBy:trackByName"
>
<div class="attributes-key-value--key">
<wp-replacement-label [fieldName]="descriptor.name">
{{ descriptor.label }}
<span
class="required"
*ngIf="descriptor.field.required && descriptor.field.writable"
>*</span>
</wp-replacement-label>
<attribute-help-text
[attribute]="descriptor.name"
[attributeScope]="'WorkPackage'"
></attribute-help-text>
</div>
<div class="attributes-key-value--value-container">
<op-editable-attribute-field
[resource]="workPackage"
[fieldName]="descriptor.name"
></op-editable-attribute-field>
</div>
</div>
</div>
</div>
<div
class="attributes-group -project-context hide-when-print"
*ngIf="!isNewResource && projectContext && !projectContext.matches"
>
<div>
<p>
<span [innerHTML]="projectContextText"></span>
<br/>
<a
[attr.href]="projectContext.href"
class="project-context--switch-link"
[textContent]="text.project.switchTo"
></a>
</p>
</div>
</div>
<ng-container *ngFor="let component of prependedAttributeGroupComponents()">
<ndc-dynamic
[ndcDynamicComponent]="component"
[ndcDynamicInputs]="{ workPackage: workPackage }"
></ndc-dynamic>
</ng-container>
<div class="attributes-group description-group">
<div class="single-attribute work-packages--details--description">
<op-editable-attribute-field
[fieldName]="'description'"
[resource]="workPackage"
[isDropTarget]="true"
[wrapperClasses]="'-no-label'"
></op-editable-attribute-field>
</div>
</div>
<div
*ngFor="let group of groupedFields; trackBy:trackByName"
[hidden]="shouldHideGroup(group)"
[attr.data-group-name]="group.name"
[ngClass]="'__overflowing_' + group.id"
[attr.data-overflowing-identifier]="'.__overflowing_' + group.id"
class="attributes-group __overflowing_element_container"
>
<ng-container wp-isolated-query-space *ngIf="group.isolated">
<ndc-dynamic
[ndcDynamicComponent]="attributeGroupComponent(group)"
[ndcDynamicInputs]="{
workPackage: workPackage,
group: group,
query: group.query
}"
></ndc-dynamic>
</ng-container>
<ng-container *ngIf="!group.isolated">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3
class="attributes-group--header-text"
[textContent]="group.name"
></h3>
</div>
</div>
<ndc-dynamic
[ndcDynamicComponent]="attributeGroupComponent(group)"
[ndcDynamicInjector]="injector"
[ndcDynamicInputs]="{ workPackage: workPackage, group: group }"
></ndc-dynamic>
</ng-container>
</div>
</div>
<div class="work-packages--attachments attributes-group" *ngIf="isNewResource">
<div class="work-packages--attachments-container">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text" [textContent]="text.attachments.label"></h3>
</div>
</div>
<ndc-dynamic
[ndcDynamicComponent]="attachmentListComponent()"
[ndcDynamicInputs]="{ resource: workPackage }"
></ndc-dynamic>
<ndc-dynamic
[ndcDynamicComponent]="attachmentUploadComponent()"
[ndcDynamicInputs]="{ resource: workPackage }"
*ngIf="workPackage.canAddAttachments"
></ndc-dynamic>
</div>
</div>
<div class="work-packages--files attributes-group" *ngIf="!isNewResource">
<div class="work-packages--files-container">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text" [textContent]="text.files.label"></h3>
</div>
</div>
<div class="attributes-group--icon-indented-text">
<op-icon icon-classes="icon-info1"></op-icon>
<span [textContent]="text.files.migration_help"></span>
<a
[textContent]="text.files.label"
[uiSref]="uiSelfRef"
[uiParams]="{ workPackageId: workPackage.id, tabIdentifier: 'files' }"
></a>
</div>
</div>
</div>

@ -86,7 +86,7 @@ export interface ResourceContextChange {
export const overflowingContainerAttribute = 'overflowingIdentifier';
@Component({
templateUrl: './wp-single-view.html',
templateUrl: './wp-single-view.component.html',
selector: 'wp-single-view',
changeDetection: ChangeDetectionStrategy.OnPush,
})
@ -142,7 +142,8 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
$element:JQuery;
constructor(readonly I18n:I18nService,
constructor(
readonly I18n:I18nService,
protected currentProject:CurrentProjectService,
protected PathHelper:PathHelperService,
protected $state:StateService,
@ -155,7 +156,8 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
protected injector:Injector,
protected cdRef:ChangeDetectorRef,
readonly elementRef:ElementRef,
readonly browserDetector:BrowserDetector) {
readonly browserDetector:BrowserDetector,
) {
super();
}

@ -1,165 +0,0 @@
<div *ngIf="workPackage"
class="work-package--single-view"
[ngClass]="{'work-package--single-view_with-columns': showTwoColumnLayout()}"
data-selector="wp-single-view">
<div class="wp-new--subject-wrapper"
*ngIf="isNewResource">
<op-editable-attribute-field [resource]="workPackage"
[wrapperClasses]="'-no-label'"
[fieldName]="'subject'"></op-editable-attribute-field>
</div>
<div class="wp-info-wrapper">
<wp-status-button *ngIf="!isNewResource"
[workPackage]="workPackage">
</wp-status-button>
<attribute-help-text [attribute]="'status'"
[attributeScope]="'WorkPackage'"
*ngIf="!isNewResource"></attribute-help-text>
<div class="work-packages--info-row"
*ngIf="!isNewResource">
<span [textContent]="idLabel"></span>:
<span [textContent]="text.infoRow.createdBy"></span>
<!-- The space has to be in an extra span
because otherwise the browser would add a second space after it -->
<span>&nbsp;</span>
<op-user-link class="user-link"
[user]="workPackage.author"></op-user-link>
<span>.&nbsp;</span>
<span [textContent]="text.infoRow.lastUpdatedOn"></span>
<span>&nbsp;</span>
<op-date-time [dateTimeValue]="workPackage.updatedAt"></op-date-time>
<span>.</span>
</div>
<wp-custom-actions [workPackage]="workPackage" class="custom-actions"></wp-custom-actions>
</div>
<div class="attributes-group -project-context __overflowing_element_container __overflowing_project_context"
*ngIf="projectContext && projectContext.field"
data-overflowing-identifier=".__overflowing_project_context">
<div>
<p class="wp-project-context--warning" [textContent]="text.project.required"></p>
<div class="attributes-key-value"
[ngClass]="{'-span-all-columns': descriptor.spanAll }"
*ngFor="let descriptor of projectContext.field; trackBy:trackByName">
<div class="attributes-key-value--key">
<wp-replacement-label [fieldName]="descriptor.name">
{{ descriptor.label }}
<span class="required"
*ngIf="descriptor.field.required && descriptor.field.writable">*</span>
</wp-replacement-label>
<attribute-help-text [attribute]="descriptor.name"
[attributeScope]="'WorkPackage'"></attribute-help-text>
</div>
<div class="attributes-key-value--value-container">
<op-editable-attribute-field [resource]="workPackage"
[fieldName]="descriptor.name"></op-editable-attribute-field>
</div>
</div>
</div>
</div>
<div
class="attributes-group -project-context hide-when-print"
*ngIf="!isNewResource && projectContext && !projectContext.matches"
>
<div>
<p>
<span [innerHTML]="projectContextText"></span>
<br/>
<a [attr.href]="projectContext.href"
class="project-context--switch-link"
[textContent]="text.project.switchTo">
</a>
</p>
</div>
</div>
<ng-container *ngFor="let component of prependedAttributeGroupComponents()">
<ndc-dynamic [ndcDynamicComponent]="component"
[ndcDynamicInputs]="{ workPackage: workPackage }">
</ndc-dynamic>
</ng-container>
<div class="attributes-group description-group">
<div class="single-attribute work-packages--details--description">
<op-editable-attribute-field [fieldName]="'description'"
[resource]="workPackage"
[isDropTarget]="true"
[wrapperClasses]="'-no-label'">
</op-editable-attribute-field>
</div>
</div>
<div
*ngFor="let group of groupedFields; trackBy:trackByName"
[hidden]="shouldHideGroup(group)"
[attr.data-group-name]="group.name"
[ngClass]="'__overflowing_' + group.id"
[attr.data-overflowing-identifier]="'.__overflowing_' + group.id"
class="attributes-group __overflowing_element_container"
>
<ng-container wp-isolated-query-space *ngIf="group.isolated">
<ndc-dynamic [ndcDynamicComponent]="attributeGroupComponent(group)"
[ndcDynamicInputs]="{ workPackage: workPackage,
group: group,
query: group.query }">
</ndc-dynamic>
</ng-container>
<ng-container *ngIf="!group.isolated">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="group.name"></h3>
</div>
</div>
<ndc-dynamic [ndcDynamicComponent]="attributeGroupComponent(group)"
[ndcDynamicInjector]="injector"
[ndcDynamicInputs]="{ workPackage: workPackage, group: group }">
</ndc-dynamic>
</ng-container>
</div>
</div>
<div class="work-packages--attachments attributes-group" *ngIf="isNewResource">
<div class="work-packages--attachments-container">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text" [textContent]="text.attachments.label"></h3>
</div>
</div>
<ndc-dynamic [ndcDynamicComponent]="attachmentListComponent()"
[ndcDynamicInputs]="{ resource: workPackage }">
</ndc-dynamic>
<ndc-dynamic [ndcDynamicComponent]="attachmentUploadComponent()"
[ndcDynamicInputs]="{ resource: workPackage }"
*ngIf="workPackage.canAddAttachments">
</ndc-dynamic>
</div>
</div>
<div class="work-packages--files attributes-group" *ngIf="!isNewResource">
<div class="work-packages--files-container">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text" [textContent]="text.files.label"></h3>
</div>
</div>
<div class="attributes-group--icon-indented-text">
<op-icon icon-classes="icon-info1"></op-icon>
<span [textContent]="text.files.migration_help"></span>
<a
[textContent]="text.files.label"
[uiSref]="uiSelfRef"
[uiParams]="{ workPackageId: workPackage.id, tabIdentifier: 'files' }"
></a>
</div>
</div>
</div>

@ -30,7 +30,7 @@ import {
Injector,
NgModule,
} from '@angular/core';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openproject-fields.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { HookService } from 'core-app/features/plugins/hook-service';
@ -179,17 +179,12 @@ import isNewResource from 'core-app/features/hal/helpers/is-new-resource';
import { OpenprojectStoragesModule } from 'core-app/shared/components/storages/openproject-storages.module';
import { FileLinksResourceService } from 'core-app/core/state/file-links/file-links.service';
import { StoragesResourceService } from 'core-app/core/state/storages/storages.service';
import { DatepickerBannerComponent } from 'core-app/shared/components/datepicker/banner/datepicker-banner.component';
import { SingleDateModalComponent } from 'core-app/shared/components/datepicker/single-date-modal/single-date.modal';
import { MultiDateModalComponent } from 'core-app/shared/components/datepicker/multi-date-modal/multi-date.modal';
import { DatepickerWorkingDaysToggleComponent } from 'core-app/shared/components/datepicker/toggle/datepicker-working-days-toggle.component';
import { DatepickerSchedulingToggleComponent } from 'core-app/shared/components/datepicker/scheduling-mode/datepicker-scheduling-toggle.component';
import { StorageFilesResourceService } from 'core-app/core/state/storage-files/storage-files.service';
@NgModule({
imports: [
// Commons
OPSharedModule,
OpSharedModule,
// Display + Edit field functionality
OpenprojectFieldsModule,
// CKEditor
@ -394,11 +389,6 @@ import { StorageFilesResourceService } from 'core-app/core/state/storage-files/s
QuerySharingModalComponent,
SaveQueryModalComponent,
WpDestroyModalComponent,
MultiDateModalComponent,
SingleDateModalComponent,
DatepickerBannerComponent,
DatepickerWorkingDaysToggleComponent,
DatepickerSchedulingToggleComponent,
// CustomActions
WpCustomActionComponent,

@ -29,11 +29,11 @@
import { NgModule } from '@angular/core';
import { MembersAutocompleterComponent } from 'core-app/shared/components/autocompleter/members-autocompleter/members-autocompleter.component';
import { NgSelectModule } from '@ng-select/ng-select';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
@NgModule({
imports: [
OPSharedModule,
OpSharedModule,
NgSelectModule,
],
exports: [],

@ -45,7 +45,7 @@ import { DateModalRelationsService } from 'core-app/shared/components/datepicker
templateUrl: './datepicker-banner.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DatepickerBannerComponent {
export class OpDatePickerBannerComponent {
@Input() scheduleManually = false;
hasRelations$ = this.dateModalRelations.hasRelations$;
@ -64,9 +64,13 @@ export class DatepickerBannerComponent {
map((relations) => relations?.length > 0),
);
isParent = this.dateModalRelations.isParent;
get isParent() {
return this.dateModalRelations.isParent;
}
isChild = this.dateModalRelations.isChild;
get isChild() {
return this.dateModalRelations.isChild;
}
text = {
automatically_scheduled_parent: this.I18n.t('js.work_packages.datepicker_modal.automatically_scheduled_parent'),

@ -30,22 +30,19 @@ import flatpickr from 'flatpickr';
import { Instance } from 'flatpickr/dist/types/instance';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { rangeSeparator } from 'core-app/shared/components/op-date-picker/op-range-date-picker/op-range-date-picker.component';
import { Injector } from '@angular/core';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { WeekdayService } from 'core-app/core/days/weekday.service';
import { rangeSeparator } from './constants';
import DateOption = flatpickr.Options.DateOption;
import { DayResourceService } from 'core-app/core/state/days/day.service';
export class DatePicker {
private datepickerFormat = 'Y-m-d';
private datepickerCont:HTMLElement = document.querySelector(this.datepickerElemIdentifier) as HTMLElement;
public datepickerInstance:Instance;
private reshowTimeout:ReturnType<typeof setTimeout>;
@InjectField() configurationService:ConfigurationService;
@InjectField() weekdaysService:WeekdayService;
@ -87,8 +84,6 @@ export class DatePicker {
}
this.datepickerInstance = Array.isArray(datePickerInstances) ? datePickerInstances[0] : datePickerInstances;
document.addEventListener('scroll', this.hideDuringScroll, true);
}
public async isNonWorkingDay(day:Date):Promise<boolean> {
@ -101,20 +96,17 @@ export class DatePicker {
public destroy():void {
this.hide();
this.datepickerInstance.destroy();
this.datepickerInstance?.destroy();
}
public hide():void {
if (this.isOpen) {
this.datepickerInstance.close();
}
document.removeEventListener('scroll', this.hideDuringScroll, true);
}
public show():void {
this.datepickerInstance.open();
document.addEventListener('scroll', this.hideDuringScroll, true);
}
public setDates(dates:DateOption|DateOption[]):void {
@ -125,50 +117,6 @@ export class DatePicker {
return this.datepickerInstance.isOpen;
}
private hideDuringScroll = (event:Event) => {
// Prevent Firefox quirk: flatPicker emits
// multiple scrolls event when it is open
const target = event.target as HTMLInputElement;
if (target?.classList?.contains('flatpickr-monthDropdown-months') || target?.classList?.contains('flatpickr-input')) {
return;
}
this.datepickerInstance.close();
if (this.reshowTimeout) {
clearTimeout(this.reshowTimeout);
}
this.reshowTimeout = setTimeout(() => {
if (this.visibleAndActive()) {
this.datepickerInstance.open();
}
}, 50);
};
private visibleAndActive() {
try {
return this.isInViewport(this.datepickerCont)
&& document.activeElement === this.datepickerCont;
} catch (e) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.error(`Failed to test visibleAndActive ${e}`);
return false;
}
}
private isInViewport(element:HTMLElement):boolean {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0
&& rect.left >= 0
&& rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)
&& rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
private get defaultOptions() {
const firstDayOfWeek = this.configurationService.startOfWeek();

@ -26,7 +26,7 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { DatePicker } from 'core-app/shared/components/op-date-picker/datepicker';
import { DatePicker } from 'core-app/shared/components/datepicker/datepicker';
import { DateOption } from 'flatpickr/dist/types/options';
import { DayElement } from 'flatpickr/dist/types/instance';
@ -102,7 +102,6 @@ export function onDayCreate(
dayElem:DayElement,
ignoreNonWorkingDays:boolean,
isNonWorkingDay:boolean,
minimalDate:Date|null|undefined,
isDayDisabled:boolean,
):void {
if (!ignoreNonWorkingDays && isNonWorkingDay) {

@ -0,0 +1,113 @@
<spot-drop-modal
[open]="isOpened"
(closed)="close()"
>
<button
slot="trigger"
type="button"
class="button"
(click)="open()"
>{{ datesString }}</button>
<form
slot="body"
class="spot-container op-datepicker-modal op-datepicker-modal_wide"
data-qa-selector="op-datepicker-modal"
tabindex="0"
cdkFocusInitial
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
(submit)="save($event)"
>
<spot-selector-field
[reverseLabel]="true"
[label]="text.ignoreNonWorkingDays.title"
>
<spot-switch
slot="input"
name="ignoreNonWorkingDays"
[(ngModel)]="ignoreNonWorkingDays"
(ngModelChange)="changeNonWorkingDays()"
data-qa-selector="op-datepicker-modal--include-non-working-days"
></spot-switch>
</spot-selector-field>
<div class="op-datepicker-modal--dates-container">
<spot-form-field
[label]="text.startDate"
>
<spot-text-field
slot="input"
name="startDate"
class="op-datepicker-modal--date-field"
[attr.data-qa-highlighted]="showFieldAsActive('start') || undefined"
[ngClass]="{'op-datepicker-modal--date-field_current' : showFieldAsActive('start')}"
[ngModel]="dates.start"
(ngModelChange)="startDateChanged$.next($event)"
[showClearButton]="currentlyActivatedDateField === 'start'"
(focusin)="setCurrentActivatedField('start')"
></spot-text-field>
<button
slot="action"
type="button"
class="spot-link"
(click)="setToday('start')"
[textContent]="text.today">
</button>
</spot-form-field>
<spot-form-field
[label]="text.endDate"
>
<spot-text-field
slot="input"
name="endDate"
class="op-datepicker-modal--date-field"
[attr.data-qa-highlighted]="showFieldAsActive('end') || undefined"
[ngClass]="{'op-datepicker-modal--date-field_current' : showFieldAsActive('end')}"
[ngModel]="dates.end"
(ngModelChange)="endDateChanged$.next($event)"
[showClearButton]="currentlyActivatedDateField === 'end'"
(focusin)="setCurrentActivatedField('end')"
></spot-text-field>
<button
slot="action"
type="button"
class="spot-link"
(click)="setToday('end')"
[textContent]="text.today">
</button>
</spot-form-field>
</div>
<input
id="flatpickr-input"
#flatpickrTarget
hidden>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
(click)="close()"
class="button spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.cancel"
></button>
<button
type="submit"
class="button -highlight spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.save"
></button>
</div>
</div>
</form>
</spot-drop-modal>
<input
[id]="id"
[name]="name"
[value]="value"
hidden
>

@ -0,0 +1,447 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2022 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-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 COPYRIGHT and LICENSE files for more details.
//++
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
HostBinding,
Injector,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
areDatesEqual,
mappedDate,
onDayCreate,
parseDate,
setDates,
validDate,
} from 'core-app/shared/components/datepicker/helpers/date-modal.helpers';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { DatePicker } from '../datepicker';
import flatpickr from 'flatpickr';
import { DayElement } from 'flatpickr/dist/types/instance';
import { ActiveDateChange, DateFields, DateKeys } from '../wp-multi-date-form/wp-multi-date-form.component';
import { fromEvent, merge, Observable, Subject } from 'rxjs';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { debounceTime, filter, map } from 'rxjs/operators';
import { DeviceService } from 'core-app/core/browser/device.service';
import { DateOption } from 'flatpickr/dist/types/options';
import { WeekdayService } from 'core-app/core/days/weekday.service';
import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper';
@Component({
selector: 'op-multi-date-picker',
templateUrl: './multi-date-picker.component.html',
styleUrls: ['../styles/datepicker.modal.sass', '../styles/datepicker_mobile.modal.sass'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpMultiDatePickerComponent),
multi: true,
},
],
})
export class OpMultiDatePickerComponent extends UntilDestroyedMixin implements OnInit, ControlValueAccessor {
@ViewChild('modalContainer') modalContainer:ElementRef<HTMLElement>;
@ViewChild('flatpickrTarget') flatpickrTarget:ElementRef;
@Input() id = `flatpickr-input-${+(new Date())}`;
@Input() name = '';
@Input() fieldName:string = '';
@Input() value:string[] = [];
@Output() valueChange = new EventEmitter();
text = {
save: this.I18n.t('js.button_save'),
cancel: this.I18n.t('js.button_cancel'),
startDate: this.I18n.t('js.work_packages.properties.startDate'),
endDate: this.I18n.t('js.work_packages.properties.dueDate'),
placeholder: this.I18n.t('js.placeholders.default'),
today: this.I18n.t('js.label_today'),
days: (count:number):string => this.I18n.t('js.units.day', { count }),
ignoreNonWorkingDays: {
title: this.I18n.t('js.work_packages.datepicker_modal.ignore_non_working_days.title'),
},
};
get datesString():string {
if (this.value?.[0] && this.value?.[1]) {
return `${this.value[0]} - ${this.value[1]}`;
}
return this.text.placeholder;
}
ignoreNonWorkingDays = false;
isOpened = false;
currentlyActivatedDateField:DateFields;
htmlId = '';
dates:{ [key in DateKeys]:string|null } = {
start: null,
end: null,
};
// Manual changes from the inputs to start and end dates
startDateChanged$ = new Subject<string>();
startDateDebounced$:Observable<ActiveDateChange> = this.debouncedInput(this.startDateChanged$, 'start');
endDateChanged$ = new Subject<string>();
endDateDebounced$:Observable<ActiveDateChange> = this.debouncedInput(this.endDateChanged$, 'end');
// Manual changes to the datepicker, with information which field was active
datepickerChanged$ = new Subject<ActiveDateChange>();
ignoreNonWorkingDaysWritable = true;
private datePickerInstance:DatePicker;
constructor(
readonly injector:Injector,
readonly cdRef:ChangeDetectorRef,
readonly I18n:I18nService,
readonly timezoneService:TimezoneService,
readonly deviceService:DeviceService,
readonly weekdayService:WeekdayService,
readonly focusHelper:FocusHelperService,
) {
super();
merge(
this.startDateDebounced$,
this.endDateDebounced$,
this.datepickerChanged$,
)
.pipe(
this.untilDestroyed(),
filter(() => !!this.datePickerInstance),
)
.subscribe(([field, update]) => {
// When clearing the one date, clear the others as well
if (update !== null) {
this.handleSingleDateUpdate(field, update);
}
this.cdRef.detectChanges();
});
}
ngOnInit(): void {
this.htmlId = `wp-datepicker-${this.fieldName as string}`;
this.setCurrentActivatedField(this.initialActivatedField);
}
open():void {
this.isOpened = true;
this.initializeDatepicker();
}
close():void {
this.isOpened = false;
this.datePickerInstance?.destroy();
}
changeNonWorkingDays():void {
this.initializeDatepicker();
this.cdRef.detectChanges();
}
save($event:Event):void {
$event.preventDefault();
const value = [
this.dates.start || '',
this.dates.end || '',
];
this.valueChange.emit(value);
this.onChange(value);
this.close();
}
updateDate(key:DateKeys, val:string|null):void {
if ((val === null || validDate(val)) && this.datePickerInstance) {
this.dates[key] = mappedDate(val);
const dateValue = parseDate(val || '') || undefined;
this.enforceManualChangesToDatepicker(dateValue);
this.cdRef.detectChanges();
}
}
setCurrentActivatedField(val:DateFields):void {
this.currentlyActivatedDateField = val;
}
toggleCurrentActivatedField():void {
this.currentlyActivatedDateField = this.currentlyActivatedDateField === 'start' ? 'end' : 'start';
}
isStateOfCurrentActivatedField(val:DateFields):boolean {
return this.currentlyActivatedDateField === val;
}
setToday(key:DateKeys):void {
this.datepickerChanged$.next([key, new Date()]);
const nextActive = key === 'start' ? 'end' : 'start';
this.setCurrentActivatedField(nextActive);
}
showFieldAsActive(field:DateFields):boolean {
return this.isStateOfCurrentActivatedField(field);
}
private initializeDatepicker(minimalDate?:Date|null) {
this.datePickerInstance?.destroy();
this.datePickerInstance = new DatePicker(
this.injector,
this.id,
[this.dates.start || '', this.dates.end || ''],
{
mode: 'range',
showMonths: this.deviceService.isMobile ? 1 : 2,
inline: true,
onReady: (_date, _datestr, instance) => {
instance.calendarContainer.classList.add('op-datepicker-modal--flatpickr-instance');
this.ensureHoveredSelection(instance.calendarContainer);
},
onChange: (dates:Date[], _datestr, instance) => {
this.onTouched();
if (dates.length === 2) {
this.setDates(dates[0], dates[1]);
this.toggleCurrentActivatedField();
this.cdRef.detectChanges();
return;
}
// Update with the same flow as entering a value
const { latestSelectedDateObj } = instance as { latestSelectedDateObj:Date };
const activeField = this.currentlyActivatedDateField;
this.handleSingleDateUpdate(activeField, latestSelectedDateObj);
this.cdRef.detectChanges();
},
onDayCreate: (dObj:Date[], dStr:string, fp:flatpickr.Instance, dayElem:DayElement) => {
onDayCreate(
dayElem,
this.ignoreNonWorkingDays,
this.weekdayService.isNonWorkingDay(dayElem.dateObj),
this.isDayDisabled(dayElem, minimalDate),
);
},
},
this.flatpickrTarget.nativeElement,
);
}
private enforceManualChangesToDatepicker(enforceDate?:Date) {
let startDate = parseDate(this.dates.start || '');
let endDate = parseDate(this.dates.end || '');
if (startDate && endDate) {
// If the start date is manually changed to be after the end date,
// we adjust the end date to be at least the same as the newly entered start date.
// Same applies if the end date is set manually before the current start date
if (startDate > endDate && this.isStateOfCurrentActivatedField('start')) {
endDate = startDate;
this.dates.end = this.timezoneService.formattedISODate(endDate);
} else if (endDate < startDate && this.isStateOfCurrentActivatedField('end')) {
startDate = endDate;
this.dates.start = this.timezoneService.formattedISODate(startDate);
}
}
const dates = [startDate, endDate];
setDates(dates, this.datePickerInstance, enforceDate);
}
private setDates(newStart:Date, newEnd:Date) {
this.dates.start = this.timezoneService.formattedISODate(newStart);
this.dates.end = this.timezoneService.formattedISODate(newEnd);
}
private handleSingleDateUpdate(activeField:DateFields, selectedDate:Date) {
if (activeField === 'duration') {
return;
}
this.replaceDatesWithNewSelection(activeField, selectedDate);
// Set the selected date on the datepicker
this.enforceManualChangesToDatepicker(selectedDate);
}
private replaceDatesWithNewSelection(activeField:DateFields, selectedDate:Date) {
/**
Overwrite flatpickr default behavior by not starting a new date range everytime but preserving either start or end date.
There are three cases to cover.
1. Everything before the current start date will become the new start date (independent of the active field)
2. Everything after the current end date will become the new end date if that is the currently active field.
If the active field is the start date, the selected date becomes the new start date and the end date is cleared.
3. Everything in between the current start and end date is dependent on the currently activated field.
* */
const parsedStartDate = parseDate(this.dates.start || '') as Date;
const parsedEndDate = parseDate(this.dates.end || '') as Date;
if (selectedDate < parsedStartDate) {
if (activeField === 'start') {
// Set start, derive end from
this.applyNewDates([selectedDate]);
} else {
// Reset and end date
this.applyNewDates(['', selectedDate]);
}
} else if (selectedDate > parsedEndDate) {
if (activeField === 'end') {
this.applyNewDates([parsedStartDate, selectedDate]);
} else {
// Reset and end date
this.applyNewDates([selectedDate]);
}
} else if (areDatesEqual(selectedDate, parsedStartDate) || areDatesEqual(selectedDate, parsedEndDate)) {
this.applyNewDates([selectedDate, selectedDate]);
} else {
const newDates = activeField === 'start' ? [selectedDate, parsedEndDate] : [parsedStartDate, selectedDate];
this.applyNewDates(newDates);
}
}
private applyNewDates([start, end]:DateOption[]) {
this.dates.start = start ? this.timezoneService.formattedISODate(start) : null;
this.dates.end = end ? this.timezoneService.formattedISODate(end) : null;
// Apply the dates to the datepicker
setDates([start, end], this.datePickerInstance);
}
private get initialActivatedField():DateFields {
switch (this.fieldName) {
case 'startDate':
return 'start';
case 'dueDate':
return 'end';
case 'duration':
return 'duration';
default:
return (this.dates.start && !this.dates.end) ? 'end' : 'start';
}
}
private isDayDisabled(dayElement:DayElement, minimalDate?:Date|null):boolean {
return !!minimalDate && dayElement.dateObj <= minimalDate;
}
private debouncedInput(input$:Subject<string>, key:DateKeys):Observable<ActiveDateChange> {
return input$
.pipe(
this.untilDestroyed(),
// Skip values that are already set as the current model
filter((value) => value !== this.dates[key]),
// Avoid that the manual changes are moved to the datepicker too early.
// The debounce is chosen quite large on purpose to catch the following case:
// 1. Start date is for example 2022-07-15. The user wants to set the end date to the 19th.
// 2. So he/she starts entering the finish date 2022-07-1 .
// 3. This is already a valid date. Since it is before the start date,the start date would be changed automatically to the first without the debounce.
// 4. The debounce gives the user enough time to type the last number "9" before the changes are converted to the datepicker and the start date would be affected.
debounceTime(500),
filter((date) => validDate(date)),
map((date) => {
if (date === '') {
return null;
}
return parseDate(date) as Date;
}),
map((date) => [key, date]),
);
}
/**
* When hovering selections in the range datepicker, the range usually
* stays active no matter where the cursor is.
*
* We want to hide any hovered selection preview when we leave the datepicker.
* @param calendarContainer
* @private
*/
private ensureHoveredSelection(calendarContainer:HTMLDivElement) {
fromEvent(calendarContainer, 'mouseenter')
.pipe(
this.untilDestroyed(),
)
.subscribe(() => calendarContainer.classList.remove('flatpickr-container-suppress-hover'));
fromEvent(calendarContainer, 'mouseleave')
.pipe(
this.untilDestroyed(),
filter(() => !(!!this.dates.start && !!this.dates.end)),
)
.subscribe(() => calendarContainer.classList.add('flatpickr-container-suppress-hover'));
}
writeValue(value:string[]|null):void {
this.value = value || [];
this.dates.start = this.value[0];
this.dates.end = this.value[1];
}
onChange = (_:string[]):void => {};
onTouched = ():void => {};
registerOnChange(fn:(_:string[]) => void):void {
this.onChange = fn;
}
registerOnTouched(fn:() => void):void {
this.onTouched = fn;
}
}

@ -17,11 +17,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatepickerSchedulingToggleComponent),
useExisting: forwardRef(() => OpDatePickerSchedulingToggleComponent),
multi: true,
}],
})
export class DatepickerSchedulingToggleComponent implements ControlValueAccessor {
export class OpDatePickerSchedulingToggleComponent implements ControlValueAccessor {
text = {
scheduling: {
title: this.I18n.t('js.scheduling.manual'),

@ -27,11 +27,8 @@
//++
import {
Inject,
Injectable,
} from '@angular/core';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { ApiV3Filter } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
import { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset';
@ -42,11 +39,13 @@ import {
shareReplay,
switchMap,
take,
tap,
} from 'rxjs/operators';
import {
combineLatest,
Observable,
of,
ReplaySubject,
Subject,
} from 'rxjs';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
@ -55,9 +54,15 @@ import { parseDate } from 'core-app/shared/components/datepicker/helpers/date-mo
@Injectable()
export class DateModalRelationsService {
private changeset:WorkPackageChangeset = this.locals.changeset as WorkPackageChangeset;
private changeset$:Subject<WorkPackageChangeset> = new ReplaySubject();
private changeset:WorkPackageChangeset;
precedingWorkPackages$:Observable<{ id:string, dueDate?:string, date?:string }[]> = of(this.changeset)
setChangeset(changeset:WorkPackageChangeset) {
this.changeset$.next(changeset);
this.changeset = changeset;
}
precedingWorkPackages$:Observable<{ id:string, dueDate?:string, date?:string }[]> = this.changeset$
.pipe(
filter((changeset) => !isNewResource(changeset.pristineResource)),
switchMap((changeset) => this
@ -76,7 +81,7 @@ export class DateModalRelationsService {
shareReplay(1),
);
followingWorkPackages$:Observable<{ id:string }[]> = of(this.changeset)
followingWorkPackages$:Observable<{ id:string }[]> = this.changeset$
.pipe(
filter((changeset) => !isNewResource(changeset.pristineResource)),
switchMap((changeset) => this
@ -100,7 +105,6 @@ export class DateModalRelationsService {
);
constructor(
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
private apiV3Service:ApiV3Service,
) {}

@ -27,26 +27,27 @@
//++
import {
Inject,
Injectable,
} from '@angular/core';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
import { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset';
import { DateModalRelationsService } from 'core-app/shared/components/datepicker/services/date-modal-relations.service';
import { DayElement } from 'flatpickr/dist/types/instance';
@Injectable()
export class DateModalSchedulingService {
private changeset:WorkPackageChangeset = this.locals.changeset as WorkPackageChangeset;
private changeset:WorkPackageChangeset;
scheduleManually = !!this.changeset.value('scheduleManually');
scheduleManually = false;
constructor(
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
readonly dateModalRelations:DateModalRelationsService,
) {}
setChangeset(changeset:WorkPackageChangeset) {
this.changeset = changeset;
this.scheduleManually = !!this.changeset.value('scheduleManually');
}
/**
* Returns whether the user can alter the dates of the work package.
*/

@ -1,74 +0,0 @@
<form
class="spot-modal op-datepicker-modal loading-indicator--location"
data-qa-selector="op-datepicker-modal"
[attr.id]="htmlId"
#modalContainer
data-indicator-name="modal"
(submit)="save($event)"
tabindex="0"
cdkFocusInitial
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<op-datepicker-banner [scheduleManually]="scheduleManually"></op-datepicker-banner>
<div class="spot-modal--body spot-container">
<div class="op-datepicker-modal--toggle-actions-container">
<op-datepicker-scheduling-toggle
name="scheduleManually"
[(ngModel)]="scheduleManually"
(ngModelChange)="changeSchedulingMode()"
></op-datepicker-scheduling-toggle>
<op-datepicker-working-days-toggle
name="ignoreNonWorkingDays"
[(ngModel)]="ignoreNonWorkingDays"
(ngModelChange)="changeNonWorkingDays()"
></op-datepicker-working-days-toggle>
</div>
<div class="op-datepicker-modal--dates-container">
<spot-form-field
[label]="text.date"
>
<spot-text-field
slot="input"
name="date"
class="op-datepicker-modal--date-field"
[ngClass]="{ 'op-datepicker-modal--date-field_current': this.dateModalScheduling.isSchedulable }"
[(ngModel)]="date"
(ngModelChange)="dateChangedManually$.next()"
[showClearButton]="true"
></spot-text-field>
<button
slot="action"
type="button"
class="spot-link"
[ngClass]="{ 'op-datepicker-modal--hidden-link': !dateModalScheduling.isSchedulable }"
(click)="setToday()"
[textContent]="text.today">
</button>
</spot-form-field>
</div>
<input id="flatpickr-input"
hidden>
</div>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
(click)="cancel()"
class="op-datepicker-modal--action spot-modal--cancel-button button button_no-margin spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.cancel"
></button>
<button
type="submit"
class="op-datepicker-modal--action button button_no-margin -highlight spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.save"
></button>
</div>
</div>
</form>

@ -0,0 +1,94 @@
<spot-drop-modal
[open]="opened"
(closed)="opened = false"
>
<ng-content
slot="trigger"
select="[slot=trigger]"
></ng-content>
<ng-template [ngIf]="useDefaultTrigger" slot="trigger">
<input
type="text"
class="spot-input"
(click)="onInputClick($event)"
[value]="value"
(focus)="opened = true"
/>
</ng-template>
<form
slot="body"
class="spot-container op-datepicker-modal"
data-qa-selector="op-datepicker-modal"
tabindex="0"
cdkFocusInitial
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
(submit)="save($event)"
>
<spot-selector-field
*ngIf="showIgnoreNonWorkingDays"
[reverseLabel]="true"
[label]="text.ignoreNonWorkingDays.title"
>
<spot-switch
slot="input"
name="ignoreNonWorkingDays"
[(ngModel)]="ignoreNonWorkingDays"
(ngModelChange)="changeNonWorkingDays()"
data-qa-selector="op-datepicker-modal--include-non-working-days"
></spot-switch>
</spot-selector-field>
<ng-content select="[slot=extra-fields]"></ng-content>
<spot-form-field
[label]="text.date"
[required]="required"
>
<spot-text-field
slot="input"
name="date"
class="op-datepicker-modal--date-field"
[ngModel]="workingValue"
[required]="required"
(ngModelChange)="writeWorkingValue($event)"
[showClearButton]="true"
></spot-text-field>
<button
slot="action"
type="button"
class="spot-link"
(click)="setToday()"
[textContent]="text.today">
</button>
</spot-form-field>
<div #flatpickrTarget></div>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
(click)="opened = false"
class="button spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.cancel"
></button>
<button
type="submit"
class="button -highlight spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.save"
></button>
</div>
</div>
</form>
</spot-drop-modal>
<input
[id]="id"
[name]="name"
[value]="value"
hidden
>

@ -0,0 +1,253 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2022 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-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 COPYRIGHT and LICENSE files for more details.
//++
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Injector,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
onDayCreate,
parseDate,
setDates,
} from 'core-app/shared/components/datepicker/helpers/date-modal.helpers';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { DatePicker } from '../datepicker';
import flatpickr from 'flatpickr';
import { DayElement } from 'flatpickr/dist/types/instance';
import { populateInputsFromDataset } from '../../dataset-inputs';
export const opSingleDatePickerSelector = 'op-single-date-picker';
@Component({
selector: opSingleDatePickerSelector,
templateUrl: './single-date-picker.component.html',
styleUrls: ['../styles/datepicker.modal.sass', '../styles/datepicker_mobile.modal.sass'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpSingleDatePickerComponent),
multi: true,
},
],
})
export class OpSingleDatePickerComponent implements ControlValueAccessor, OnInit, AfterContentInit {
@Output('closed') closed = new EventEmitter();
@Output('valueChange') valueChange = new EventEmitter();
private _value = '';
@Input() set value(newValue:string) {
this._value = newValue;
this.writeWorkingValue(newValue);
}
get value() {
return this._value;
}
@Input() id = `flatpickr-input-${+(new Date())}`;
@Input() name = '';
@Input() required = false;
@Input() minimalDate:Date|null = null;
private _opened = false;
@Input() set opened(opened:boolean) {
if (this._opened === !!opened) {
return;
}
this._opened = !!opened;
if (this._opened) {
this.initializeDatepicker();
} else {
this.closed.emit();
}
}
get opened() {
return this._opened;
}
@Input() showIgnoreNonWorkingDays = true;
@Input() ignoreNonWorkingDays = false;
@ViewChild('flatpickrTarget') flatpickrTarget:ElementRef;
public workingValue = '';
public workingDate:Date = new Date();
public datePickerInstance:DatePicker;
public useDefaultTrigger = false;
text = {
save: this.I18n.t('js.button_save'),
cancel: this.I18n.t('js.button_cancel'),
date: this.I18n.t('js.work_packages.properties.date'),
placeholder: this.I18n.t('js.placeholders.default'),
today: this.I18n.t('js.label_today'),
ignoreNonWorkingDays: {
title: this.I18n.t('js.work_packages.datepicker_modal.ignore_non_working_days.title'),
},
};
constructor(
readonly I18n:I18nService,
readonly timezoneService:TimezoneService,
readonly injector:Injector,
readonly cdRef:ChangeDetectorRef,
readonly elementRef:ElementRef,
) {
populateInputsFromDataset(this);
}
ngOnInit(): void {
if (!this.value) {
const today = parseDate(new Date()) as Date;
this.writeValue(this.timezoneService.formattedISODate(today));
}
}
ngAfterContentInit() {
const trigger = this.elementRef.nativeElement.querySelector("[slot='trigger']");
this.useDefaultTrigger = trigger === null;
}
onInputClick(event:MouseEvent) {
event.stopPropagation();
}
save($event:SubmitEvent) {
const form = $event.target as HTMLFormElement;
if (form.reportValidity()) {
$event.preventDefault();
this.valueChange.emit(this.workingValue);
this.onChange(this.workingValue);
this.writeValue(this.workingValue);
this.opened = false;
}
}
setToday():void {
const today = parseDate(new Date()) as Date;
this.writeWorkingValue(this.timezoneService.formattedISODate(today));
this.enforceManualChangesToDatepicker(today);
}
changeNonWorkingDays():void {
this.initializeDatepicker();
this.cdRef.detectChanges();
}
private enforceManualChangesToDatepicker(enforceDate?:Date) {
const date = parseDate(this.workingDate || '');
setDates(date, this.datePickerInstance, enforceDate);
}
private initializeDatepicker() {
this.datePickerInstance?.destroy();
this.datePickerInstance = new DatePicker(
this.injector,
this.id,
this.workingDate || '',
{
mode: 'single',
showMonths: 1,
inline: true,
onReady: (_date:Date[], _datestr:string, instance:flatpickr.Instance) => {
instance.calendarContainer.classList.add('op-datepicker-modal--flatpickr-instance');
},
onChange: (dates:Date[]) => {
if (dates.length > 0) {
const dateString = this.timezoneService.formattedISODate(dates[0]);
this.writeWorkingValue(dateString);
this.enforceManualChangesToDatepicker(dates[0]);
this.onTouched(dateString);
}
this.cdRef.detectChanges();
},
onDayCreate: (dObj:Date[], dStr:string, fp:flatpickr.Instance, dayElem:DayElement) => {
onDayCreate(
dayElem,
!this.ignoreNonWorkingDays,
this.datePickerInstance?.weekdaysService.isNonWorkingDay(dayElem.dateObj),
!!this.minimalDate && dayElem.dateObj <= this.minimalDate,
);
},
},
this.flatpickrTarget.nativeElement,
);
}
writeWorkingValue(value:string):void {
this.workingValue = value;
this.workingDate = new Date(value);
}
writeValue(value:string):void {
this.writeWorkingValue(value);
this.value = value;
}
onChange = (_:string):void => {};
onTouched = (_:string):void => {};
registerOnChange(fn:(_:string) => void):void {
this.onChange = fn;
}
registerOnTouched(fn:(_:string) => void):void {
this.onTouched = fn;
}
}

@ -1,16 +1,14 @@
@import '../../app/spot/styles/sass/variables'
.op-datepicker-modal
z-index: 500
width: auto
min-height: 200px
width: 100vw
// Basically the width of the two calendars next to each other + spacings
// will be overwritten on mobile
max-width: 600px
min-height: 200px
box-shadow: $spot-shadow-light-mid
// Apply a bottom margin before the banners are loaded
// to avoid overlapping
margin-bottom: 60px
max-width: 300px
&_wide
max-width: 600px
&--toggle-actions-container
display: grid
@ -37,3 +35,6 @@
&--hidden-link
visibility: hidden
&--flatpickr-instance
margin: 0 auto

@ -17,11 +17,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatepickerWorkingDaysToggleComponent),
useExisting: forwardRef(() => OpDatePickerWorkingDaysToggleComponent),
multi: true,
}],
})
export class DatepickerWorkingDaysToggleComponent implements ControlValueAccessor {
export class OpDatePickerWorkingDaysToggleComponent implements ControlValueAccessor {
@Input() ignoreNonWorkingDays:boolean;
@Input() disabled = false;

@ -1,5 +1,5 @@
<form
class="spot-modal op-datepicker-modal loading-indicator--location"
class="spot-modal"
data-qa-selector="op-datepicker-modal"
[attr.id]="htmlId"
#modalContainer
@ -8,11 +8,11 @@
cdkFocusInitial
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
(submit)="save($event)"
(submit)="doSave($event)"
>
<op-datepicker-banner [scheduleManually]="scheduleManually"></op-datepicker-banner>
<div class="spot-modal--body spot-container form -vertical">
<div class="spot-modal--body spot-container op-datepicker-modal op-datepicker-modal_wide">
<div class="op-datepicker-modal--toggle-actions-container">
<op-datepicker-scheduling-toggle
name="scheduleManually"
@ -104,15 +104,18 @@
</spot-form-field>
</div>
<input id="flatpickr-input"
hidden>
<input
#flatpickrTarget
id="flatpickr-input"
hidden
/>
</div>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
(click)="cancel()"
(click)="doCancel()"
class="op-datepicker-modal--action spot-modal--cancel-button button button_no-margin spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.cancel"

@ -33,20 +33,17 @@ import {
Component,
ElementRef,
EventEmitter,
Inject,
Input,
Injector,
ViewChild,
ViewEncapsulation,
OnInit,
Output,
HostBinding,
} from '@angular/core';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { DatePicker } from 'core-app/shared/components/op-date-picker/datepicker';
import { HalResourceEditingService } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/resource-changeset';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { DayElement } from 'flatpickr/dist/types/instance';
import flatpickr from 'flatpickr';
@ -56,7 +53,6 @@ import {
map,
switchMap,
} from 'rxjs/operators';
import { activeFieldContainerClassName } from 'core-app/shared/components/fields/edit/edit-form/edit-form';
import {
fromEvent,
merge,
@ -78,16 +74,21 @@ import {
import { WeekdayService } from 'core-app/core/days/weekday.service';
import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper';
import { DeviceService } from 'core-app/core/browser/device.service';
import { DatePicker } from '../datepicker';
import DateOption = flatpickr.Options.DateOption;
import { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import isNewResource from 'core-app/features/hal/helpers/is-new-resource';
export type DateKeys = 'start'|'end';
export type DateFields = DateKeys|'duration';
type StartUpdate = { startDate:string };
type EndUpdate = { dueDate:string };
type DurationUpdate = { duration:string|number|null };
type DateUpdate = { date:string };
type ActiveDateChange = [DateFields, null|Date|Date];
export type StartUpdate = { startDate:string };
export type EndUpdate = { dueDate:string };
export type DurationUpdate = { duration:string|number|null };
export type DateUpdate = { date:string };
export type ActiveDateChange = [DateFields, null|Date|Date];
export type FieldUpdates =
StartUpdate
@ -98,8 +99,12 @@ export type FieldUpdates =
|DateUpdate;
@Component({
templateUrl: './multi-date.modal.html',
styleUrls: ['../styles/datepicker.modal.sass', '../styles/datepicker_mobile.modal.sass'],
selector: 'op-wp-multi-date-form',
templateUrl: './wp-multi-date-form.component.html',
styleUrls: [
'../styles/datepicker.modal.sass',
'../styles/datepicker_mobile.modal.sass',
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [
@ -107,26 +112,24 @@ export type FieldUpdates =
DateModalRelationsService,
],
})
export class MultiDateModalComponent extends OpModalComponent implements AfterViewInit {
@InjectField() I18n!:I18nService;
@InjectField() timezoneService:TimezoneService;
export class OpWpMultiDateFormComponent extends UntilDestroyedMixin implements AfterViewInit, OnInit {
@HostBinding('class.op-datepicker-modal') className = true;
@InjectField() halEditing:HalResourceEditingService;
@HostBinding('class.op-datepicker-modal_wide') classNameWide = true;
@InjectField() dateModalScheduling:DateModalSchedulingService;
@ViewChild('modalContainer') modalContainer:ElementRef<HTMLElement>;
@InjectField() dateModalRelations:DateModalRelationsService;
@ViewChild('durationField', { read: ElementRef }) durationField:ElementRef<HTMLElement>;
@InjectField() deviceService:DeviceService;
@ViewChild('flatpickrTarget') flatpickrTarget:ElementRef;
@InjectField() weekdayService:WeekdayService;
@Input() changeset:ResourceChangeset;
@InjectField() focusHelper:FocusHelperService;
@Input() fieldName:string = '';
@ViewChild('modalContainer') modalContainer:ElementRef<HTMLElement>;
@Output() cancel = new EventEmitter();
@ViewChild('durationField', { read: ElementRef }) durationField:ElementRef<HTMLElement>;
@Output() save = new EventEmitter();
text = {
save: this.I18n.t('js.button_save'),
@ -139,8 +142,6 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
days: (count:number):string => this.I18n.t('js.units.day', { count }),
};
onDataUpdated = new EventEmitter<string>();
scheduleManually = false;
ignoreNonWorkingDays = false;
@ -194,7 +195,6 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
this.clearWithDuration(field);
}
this.onDataChange();
this.cdRef.detectChanges();
});
@ -216,43 +216,51 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
// which is different from the highlight state...
durationFocused = false;
private changeset:ResourceChangeset;
ignoreNonWorkingDaysWritable = true;
private datePickerInstance:DatePicker;
private formUpdates$ = new Subject<FieldUpdates>();
private dateUpdateRequests$ = this
.formUpdates$
.pipe(
this.untilDestroyed(),
switchMap((fieldsToUpdate:FieldUpdates) => this
.apiV3Service
.work_packages
.withOptionalId(this.changeset.id === 'new' ? null : this.changeset.id)
.form
.forPayload({
...fieldsToUpdate,
lockVersion: this.changeset.value<string>('lockVersion'),
ignoreNonWorkingDays: this.ignoreNonWorkingDays,
scheduleManually: this.scheduleManually,
})),
)
.subscribe((form) => this.updateDatesFromForm(form));
constructor(
readonly injector:Injector,
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
readonly cdRef:ChangeDetectorRef,
readonly elementRef:ElementRef,
readonly configurationService:ConfigurationService,
readonly apiV3Service:ApiV3Service,
readonly I18n:I18nService,
readonly timezoneService:TimezoneService,
readonly halEditing:HalResourceEditingService,
readonly dateModalScheduling:DateModalSchedulingService,
readonly dateModalRelations:DateModalRelationsService,
readonly deviceService:DeviceService,
readonly weekdayService:WeekdayService,
readonly focusHelper:FocusHelperService,
) {
super(locals, cdRef, elementRef);
this.changeset = locals.changeset as ResourceChangeset;
this.htmlId = `wp-datepicker-${locals.fieldName as string}`;
super();
this
.formUpdates$
.pipe(
this.untilDestroyed(),
switchMap((fieldsToUpdate:FieldUpdates) => this
.apiV3Service
.work_packages
.withOptionalId(this.changeset.id === 'new' ? null : this.changeset.id)
.form
.forPayload({
...fieldsToUpdate,
lockVersion: this.changeset.value<string>('lockVersion'),
ignoreNonWorkingDays: this.ignoreNonWorkingDays,
scheduleManually: this.scheduleManually,
})),
)
.subscribe((form) => this.updateDatesFromForm(form));
}
ngOnInit(): void {
this.htmlId = `wp-datepicker-${this.fieldName as string}`;
this.dateModalScheduling.setChangeset(this.changeset as WorkPackageChangeset);
this.dateModalRelations.setChangeset(this.changeset as WorkPackageChangeset);
this.scheduleManually = !!this.changeset.value('scheduleManually');
this.ignoreNonWorkingDays = !!this.changeset.value('ignoreNonWorkingDays');
@ -275,18 +283,24 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
}
ngAfterViewInit():void {
const init = (date:Date|null) => {
this.initializeDatepicker(date);
// Autofocus duration if that's what activated us
if (this.initialActivatedField === 'duration') {
this.focusHelper.focus(this.durationField.nativeElement);
}
};
if (isNewResource(this.changeset.pristineResource)) {
init(null);
return;
}
this
.dateModalRelations
.getMinimalDateFromPreceeding()
.subscribe((date) => {
this.initializeDatepicker(date);
this.onDataChange();
});
// Autofocus duration if that's what activated us
if (this.initialActivatedField === 'duration') {
this.focusHelper.focus(this.durationField.nativeElement);
}
.subscribe((date) => init(date));
}
changeSchedulingMode():void {
@ -321,7 +335,7 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
this.cdRef.detectChanges();
}
save($event:Event):void {
doSave($event:Event):void {
$event.preventDefault();
// Apply the changed scheduling mode if any
this.changeset.setValue('scheduleManually', this.scheduleManually);
@ -336,11 +350,11 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
this.changeset.setValue('duration', this.durationAsIso8601);
}
this.closeMe();
this.save.emit();
}
cancel():void {
this.closeMe();
doCancel():void {
this.cancel.emit();
}
updateDate(key:DateKeys, val:string|null):void {
@ -371,20 +385,6 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
this.setCurrentActivatedField(nextActive);
}
// eslint-disable-next-line class-methods-use-this
reposition(element:JQuery<HTMLElement>, target:JQuery<HTMLElement>):void {
if (this.deviceService.isMobile) {
return;
}
element.position({
my: 'left top',
at: 'left bottom',
of: target,
collision: 'flipfit',
});
}
showTodayLink():boolean {
return this.isSchedulable;
}
@ -471,7 +471,6 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
instance.calendarContainer.classList.add('op-datepicker-modal--flatpickr-instance');
if (!this.modalPositioned) {
this.reposition(jQuery(this.modalContainer.nativeElement), jQuery(`.${activeFieldContainerClassName}`));
this.modalPositioned = true;
}
@ -504,12 +503,11 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
dayElem,
this.ignoreNonWorkingDays,
await this.datePickerInstance?.isNonWorkingDay(dayElem.dateObj),
minimalDate,
this.dateModalScheduling.isDayDisabled(dayElem, minimalDate),
this.isDayDisabled(dayElem, minimalDate),
);
},
},
null,
this.flatpickrTarget.nativeElement,
);
}
@ -532,7 +530,6 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
const dates = [startDate, endDate];
setDates(dates, this.datePickerInstance, enforceDate);
this.onDataChange();
}
private setDatesAndDeriveDuration(newStart:Date, newEnd:Date) {
@ -735,16 +732,8 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
}
}
private onDataChange() {
const start = this.dates.start || '';
const end = this.dates.end || '';
const output = `${start} - ${end}`;
this.onDataUpdated.emit(output);
}
private get initialActivatedField():DateFields {
switch (this.locals.fieldName) {
switch (this.fieldName) {
case 'startDate':
return 'start';
case 'dueDate':

@ -0,0 +1,70 @@
<form
class="spot-container"
data-qa-selector="op-datepicker-modal"
[attr.id]="htmlId"
#modalContainer
data-indicator-name="modal"
(submit)="doSave($event)"
tabindex="0"
cdkFocusInitial
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<op-datepicker-banner></op-datepicker-banner>
<div class="spot-container">
<div class="op-datepicker-modal--toggle-actions-container">
<op-datepicker-working-days-toggle
name="ignoreNonWorkingDays"
[(ngModel)]="ignoreNonWorkingDays"
(ngModelChange)="changeNonWorkingDays()"
></op-datepicker-working-days-toggle>
</div>
<spot-form-field
[label]="text.date"
>
<spot-text-field
slot="input"
name="date"
class="op-datepicker-modal--date-field"
[ngClass]="{ 'op-datepicker-modal--date-field_current': this.dateModalScheduling.isSchedulable }"
[(ngModel)]="date"
(ngModelChange)="dateChangedManually$.next()"
[showClearButton]="true"
></spot-text-field>
<button
slot="action"
type="button"
class="spot-link"
[ngClass]="{ 'op-datepicker-modal--hidden-link': !dateModalScheduling.isSchedulable }"
(click)="setToday()"
[textContent]="text.today">
</button>
</spot-form-field>
<input
#flatpickrTarget
id="flatpickr-input"
hidden
>
</div>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
(click)="doCancel()"
class="op-datepicker-modal--action spot-modal--cancel-button button button_no-margin spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.cancel"
></button>
<button
type="submit"
class="op-datepicker-modal--action button button_no-margin -highlight spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"
[textContent]="text.save"
></button>
</div>
</div>
</form>

@ -33,29 +33,23 @@ import {
Component,
ElementRef,
EventEmitter,
Inject,
OnInit,
Injector,
Input,
Output,
ViewChild,
ViewEncapsulation,
HostBinding,
} from '@angular/core';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { DatePicker } from 'core-app/shared/components/op-date-picker/datepicker';
import { DatePicker } from 'core-app/shared/components/datepicker/datepicker';
import { HalResourceEditingService } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/resource-changeset';
import { BrowserDetector } from 'core-app/core/browser/browser-detector.service';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { DayElement } from 'flatpickr/dist/types/instance';
import flatpickr from 'flatpickr';
import {
debounce,
switchMap,
} from 'rxjs/operators';
import { activeFieldContainerClassName } from 'core-app/shared/components/fields/edit/edit-form/edit-form';
import { debounce } from 'rxjs/operators';
import {
Subject,
timer,
@ -71,11 +65,17 @@ import {
setDates,
validDate,
} from 'core-app/shared/components/datepicker/helpers/date-modal.helpers';
import { DeviceService } from 'core-app/core/browser/device.service';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset';
import isNewResource from 'core-app/features/hal/helpers/is-new-resource';
@Component({
templateUrl: './single-date.modal.html',
styleUrls: ['../styles/datepicker.modal.sass', '../styles/datepicker_mobile.modal.sass'],
selector: 'op-wp-single-date-form',
templateUrl: './wp-single-date-form.component.html',
styleUrls: [
'../styles/datepicker.modal.sass',
'../styles/datepicker_mobile.modal.sass',
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [
@ -83,18 +83,18 @@ import { DeviceService } from 'core-app/core/browser/device.service';
DateModalRelationsService,
],
})
export class SingleDateModalComponent extends OpModalComponent implements AfterViewInit {
@InjectField() I18n!:I18nService;
export class OpWpSingleDateFormComponent extends UntilDestroyedMixin implements AfterViewInit, OnInit {
@HostBinding('class.op-datepicker-modal') className = true;
@InjectField() timezoneService:TimezoneService;
@Input('value') value = '';
@InjectField() halEditing:HalResourceEditingService;
@Input() changeset:ResourceChangeset;
@InjectField() dateModalScheduling:DateModalSchedulingService;
@Output() cancel = new EventEmitter();
@InjectField() dateModalRelations:DateModalRelationsService;
@Output() save = new EventEmitter();
@InjectField() deviceService:DeviceService;
@ViewChild('flatpickrTarget') flatpickrTarget:ElementRef;
@ViewChild('modalContainer') modalContainer:ElementRef<HTMLElement>;
@ -106,10 +106,6 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
today: this.I18n.t('js.label_today'),
};
onDataUpdated = new EventEmitter<string>();
scheduleManually = false;
ignoreNonWorkingDays = false;
htmlId = '';
@ -120,54 +116,44 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
private debounceDelay = 0; // will change after initial render
private changeset:ResourceChangeset;
private datePickerInstance:DatePicker;
private dateUpdates$ = new Subject<string>();
private dateUpdateRequests$ = this
.dateUpdates$
.pipe(
this.untilDestroyed(),
switchMap((date:string) => this
.apiV3Service
.work_packages
.id(this.changeset.id)
.form
.forPayload({
date,
lockVersion: this.changeset.value<string>('lockVersion'),
ignoreNonWorkingDays: this.ignoreNonWorkingDays,
})),
)
.subscribe((form) => this.updateDatesFromForm(form));
constructor(
readonly injector:Injector,
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
readonly cdRef:ChangeDetectorRef,
readonly elementRef:ElementRef,
readonly configurationService:ConfigurationService,
readonly apiV3Service:ApiV3Service,
readonly cdRef:ChangeDetectorRef,
readonly injector:Injector,
readonly I18n:I18nService,
readonly timezoneService:TimezoneService,
readonly halEditing:HalResourceEditingService,
readonly dateModalScheduling:DateModalSchedulingService,
readonly dateModalRelations:DateModalRelationsService,
) {
super(locals, cdRef, elementRef);
this.changeset = locals.changeset as ResourceChangeset;
this.htmlId = `wp-datepicker-${locals.fieldName as string}`;
this.ignoreNonWorkingDays = !!this.changeset.value('ignoreNonWorkingDays');
this.date = this.changeset.value('date');
super();
}
ngOnInit():void {
this.dateModalRelations.setChangeset(this.changeset as WorkPackageChangeset);
this.dateModalScheduling.setChangeset(this.changeset as WorkPackageChangeset);
if (!moment(this.value).isValid()) {
this.value = '';
this.date = '';
return;
}
this.date = this.timezoneService.formattedISODate(this.value);
}
ngAfterViewInit():void {
this
.dateModalRelations
.getMinimalDateFromPreceeding()
.subscribe((date) => {
this.initializeDatepicker(date);
this.onDataChange();
});
if (isNewResource(this.changeset.pristineResource)) {
this.initializeDatepicker(null);
} else {
this
.dateModalRelations
.getMinimalDateFromPreceeding()
.subscribe((date) => {
this.initializeDatepicker(date);
});
}
this
.dateChangedManually$
@ -191,28 +177,13 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
});
}
changeSchedulingMode():void {
this.initializeDatepicker();
this.cdRef.detectChanges();
}
changeNonWorkingDays():void {
this.initializeDatepicker();
// If we're single date, update the date
if (!this.ignoreNonWorkingDays && this.date) {
// Resent the current start and end dates so duration can be calculated again.
this.dateUpdates$.next(this.date);
}
this.cdRef.detectChanges();
}
save($event:Event):void {
doSave($event:Event):void {
$event.preventDefault();
// Apply the changed scheduling mode if any
this.changeset.setValue('scheduleManually', this.scheduleManually);
// Apply include NWD
this.changeset.setValue('ignoreNonWorkingDays', this.ignoreNonWorkingDays);
@ -221,11 +192,11 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
this.changeset.setValue('date', mappedDate(this.date));
}
this.closeMe();
this.save.emit();
}
cancel():void {
this.closeMe();
doCancel():void {
this.cancel.emit();
}
updateDate(val:string|null):void {
@ -244,20 +215,6 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
this.enforceManualChangesToDatepicker(today);
}
// eslint-disable-next-line class-methods-use-this
reposition(element:JQuery<HTMLElement>, target:JQuery<HTMLElement>):void {
if (this.deviceService.isMobile) {
return;
}
element.position({
my: 'left top',
at: 'left bottom',
of: target,
collision: 'flipfit',
});
}
private initializeDatepicker(minimalDate?:Date|null) {
this.datePickerInstance?.destroy();
this.datePickerInstance = new DatePicker(
@ -266,11 +223,10 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
this.date || '',
{
mode: 'single',
showMonths: this.deviceService.isMobile ? 1 : 2,
showMonths: 1,
inline: true,
onReady: (_date, _datestr, instance) => {
onReady: (_date:Date[], _datestr:string, instance:flatpickr.Instance) => {
instance.calendarContainer.classList.add('op-datepicker-modal--flatpickr-instance');
this.reposition(jQuery(this.modalContainer.nativeElement), jQuery(`.${activeFieldContainerClassName}`));
},
onChange: (dates:Date[]) => {
if (dates.length > 0) {
@ -278,7 +234,6 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
this.enforceManualChangesToDatepicker(dates[0]);
}
this.onDataChange();
this.cdRef.detectChanges();
},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@ -287,22 +242,21 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
dayElem,
this.ignoreNonWorkingDays,
await this.datePickerInstance?.isNonWorkingDay(dayElem.dateObj),
minimalDate,
this.dateModalScheduling.isDayDisabled(dayElem, minimalDate),
);
},
},
null,
this.flatpickrTarget.nativeElement,
);
}
private enforceManualChangesToDatepicker(enforceDate?:Date) {
const date = parseDate(this.date || '');
setDates(date, this.datePickerInstance, enforceDate);
}
private onDataChange() {
this.onDataUpdated.emit(this.date || '');
if (date) {
this.date = this.timezoneService.formattedISODate(date);
}
}
/**

@ -1,81 +0,0 @@
import {
AfterViewInit, ChangeDetectorRef, Component, forwardRef, NgZone,
} from '@angular/core';
import { OpSingleDatePickerComponent } from 'core-app/shared/components/op-date-picker/op-single-date-picker/op-single-date-picker.component';
import * as moment from 'moment';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
@Component({
selector: 'op-date-picker-adapter',
templateUrl: '../../../../../../op-date-picker/op-single-date-picker/op-single-date-picker.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatePickerAdapterComponent),
multi: true,
},
],
})
export class DatePickerAdapterComponent extends OpSingleDatePickerComponent implements AfterViewInit {
onControlChange = (_:any) => { };
onControlTouch = () => { };
writeValue(date:string):void {
this.initialDate = this.formatter(date);
}
registerOnChange(fn:(_:any) => void):void {
this.onControlChange = fn;
}
registerOnTouched(fn:any):void {
this.onControlTouch = fn;
}
setDisabledState(disabled:boolean):void {
this.disabled = disabled;
}
ngAfterViewInit():void {
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
this.initializeDatepicker();
this.changeDetectorRef.detectChanges();
});
});
}
onInputChange():void {
if (this.isEmpty()) {
this.datePickerInstance.clear();
} else if (this.inputIsValidDate()) {
const valueToEmit = this.parser(this.currentValue);
this.onControlTouch();
this.onControlChange(valueToEmit);
}
}
closeOnOutsideClick(event:any) {
super.closeOnOutsideClick(event);
this.onControlTouch();
}
public parser(data:any) {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
return data;
}
return null;
}
public formatter(data:any):string {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
const d = this.timezoneService.parseDate(data);
return this.timezoneService.formattedISODate(d);
}
return '';
}
}

@ -1,91 +0,0 @@
import {
AfterViewInit,
Component,
forwardRef,
Input,
} from '@angular/core';
import * as moment from 'moment';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { OpSingleDatePickerComponent } from 'core-app/shared/components/op-date-picker/op-single-date-picker/op-single-date-picker.component';
/* eslint-disable-next-line change-detection-strategy/on-push */
@Component({
selector: 'op-date-picker-control',
templateUrl: '../../../../../../op-date-picker/op-single-date-picker/op-single-date-picker.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatePickerControlComponent),
multi: true,
},
],
})
export class DatePickerControlComponent extends OpSingleDatePickerComponent implements ControlValueAccessor, AfterViewInit {
// Avoid Angular warning (It looks like you're using the disabled attribute with a reactive form directive...)
/* eslint-disable-next-line @angular-eslint/no-input-rename */
@Input('disable') disabled:boolean;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
onControlChange:(_?:unknown) => void = () => { };
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
onControlTouch:(_?:unknown) => void = () => { };
writeValue(date:string):void {
this.initialDate = this.formatter(date);
}
registerOnChange(fn:(_:unknown) => void):void {
this.onControlChange = fn;
}
registerOnTouched(fn:(_:unknown) => void):void {
this.onControlTouch = fn;
}
setDisabledState(disabled:boolean):void {
this.disabled = disabled;
}
ngAfterViewInit():void {
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
this.initializeDatepicker();
this.changeDetectorRef.detectChanges();
});
});
}
onInputChange():void {
const valueToEmit = this.inputIsValidDate()
? this.parser(this.currentValue)
: '';
this.onControlChange(valueToEmit);
this.onControlTouch();
}
closeOnOutsideClick(event:MouseEvent):void {
super.closeOnOutsideClick(event);
this.onControlTouch();
}
public parser(data:string):string|null {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
return data;
}
return null;
}
public formatter(data:string):string {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
const d = this.timezoneService.parseDate(data);
return this.timezoneService.formattedISODate(d);
}
return '';
}
}

@ -1,18 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DatePickerModule } from 'core-app/shared/components/op-date-picker/date-picker.module';
import { DatePickerControlComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-control/date-picker-control.component';
@NgModule({
declarations: [
DatePickerControlComponent,
],
imports: [
CommonModule,
DatePickerModule,
],
exports: [
DatePickerControlComponent,
],
})
export class DatePickerControlModule { }

@ -1,7 +1,6 @@
<op-date-picker-control
<op-single-date-picker
[required]="to.required"
[disable]="to.disabled"
[disabled]="to.disabled"
[formControl]="formControl"
[formlyAttributes]="field"
>
</op-date-picker-control>
></op-single-date-picker>

@ -9,7 +9,6 @@ import { IntegerInputComponent } from 'core-app/shared/components/dynamic-forms/
import { SelectInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/select-input/select-input.component';
import { ProjectInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/project-input/project-input.component';
import { SelectProjectStatusInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/select-project-status-input/select-project-status-input.component';
import { DatePickerControlModule } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-control/date-picker-control.module';
import { BooleanInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/boolean-input/boolean-input.component';
import { DynamicFormComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component';
import { FormattableTextareaInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/formattable-textarea-input/formattable-textarea-input.component';
@ -18,15 +17,17 @@ import { InviteUserButtonModule } from 'core-app/features/invite-user-modal/butt
import { DateInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/date-input.component';
import { DynamicFieldGroupWrapperComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-field-group-wrapper/dynamic-field-group-wrapper.component';
import { FormattableControlModule } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/formattable-textarea-input/components/formattable-control/formattable-control.module';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { UserInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/user-input/user-input.component';
import { AttributeHelpTextModule } from 'core-app/shared/components/attribute-help-texts/attribute-help-text.module';
import { OpSpotModule } from 'core-app/spot/spot.module';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
AttributeHelpTextModule,
OpSpotModule,
FormlyModule.forRoot({
types: [
{ name: 'booleanInput', component: BooleanInputComponent },
@ -50,13 +51,12 @@ import { AttributeHelpTextModule } from 'core-app/shared/components/attribute-he
},
],
}),
OPSharedModule,
OpSharedModule,
// Input dependencies
NgSelectModule,
NgOptionHighlightModule,
InviteUserButtonModule,
DatePickerControlModule,
FormattableControlModule,
],
declarations: [

@ -47,7 +47,7 @@ import { WorkPackageCommentFieldComponent } from 'core-app/features/work-package
import { ProjectEditFieldComponent } from './field-types/project-edit-field.component';
import { HoursDurationEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/hours-duration-edit-field.component';
import { UserEditFieldComponent } from './field-types/user-edit-field.component';
import { DaysDurationEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/days-duration-edit-field.compontent';
import { DaysDurationEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/days-duration-edit-field.component';
export function initializeCoreEditFields(editFieldService:EditFieldService, selectAutocompleterRegisterService:SelectAutocompleterRegisterService) {
return ():void => {

@ -34,6 +34,8 @@ export interface IEditFieldType extends IFieldType<EditFieldComponent> {
new():EditFieldComponent;
}
@Injectable({ providedIn: 'root' })
@Injectable({
providedIn: 'root',
})
export class EditFieldService extends AbstractFieldService<EditFieldComponent, IEditFieldType> {
}

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EditFieldControlsComponent } from 'core-app/shared/components/fields/edit/field-controls/edit-field-controls.component';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
@NgModule({
declarations: [
@ -9,7 +9,7 @@ import { OPSharedModule } from 'core-app/shared/shared.module';
],
imports: [
CommonModule,
OPSharedModule,
OpSharedModule,
],
exports: [
EditFieldControlsComponent,

@ -0,0 +1,32 @@
<spot-drop-modal
[open]="opened"
(closed)="cancel()"
alignment="bottom-center"
>
<input
slot="trigger"
type="text"
class="spot-input"
(click)="onInputClick($event)"
[value]="dates"
(focus)="showDatePickerModal()"
/>
<op-wp-single-date-form
*ngIf="opened && !isMultiDate"
[value]="dates"
[changeset]="change"
(save)="save()"
(cancel)="cancel()"
slot="body"
></op-wp-single-date-form>
<op-wp-multi-date-form
*ngIf="opened && isMultiDate"
[value]="dates"
[changeset]="change"
[fieldName]="name"
(save)="save()"
(cancel)="cancel()"
slot="body"
></op-wp-multi-date-form>
</spot-drop-modal>

@ -31,16 +31,13 @@ import { DatePickerEditFieldComponent } from 'core-app/shared/components/fields/
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
@Component({
template: `
<input [value]="dates"
(click)="showDatePickerModal()"
class="op-input"
type="text" />
`,
templateUrl: './combined-date-edit-field.component.html',
})
export class CombinedDateEditFieldComponent extends DatePickerEditFieldComponent {
dates = '';
opened = false;
text = {
placeholder: {
startDate: this.I18n.t('js.label_no_start_date'),
@ -49,23 +46,33 @@ export class CombinedDateEditFieldComponent extends DatePickerEditFieldComponent
},
};
get isMultiDate():boolean {
return !this.change.schema.isMilestone;
}
public onInputClick(event:MouseEvent) {
event.stopPropagation();
}
public showDatePickerModal():void {
super.showDatePickerModal();
this
.modal
?.onDataUpdated
.subscribe((dates:string) => {
this.dates = dates;
this.cdRef.detectChanges();
});
this.opened = true;
}
protected onModalClosed():void {
public onModalClosed():void {
this.opened = false;
this.resetDates();
super.onModalClosed();
}
public save():void {
this.handler.handleUserSubmit();
this.onModalClosed();
}
public cancel():void {
this.handler.reset();
}
// Overwrite super in order to set the initial dates.
protected initialize():void {
super.initialize();

@ -26,44 +26,35 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import * as moment from 'moment';
import { EditFieldComponent } from 'core-app/shared/components/fields/edit/edit-field.component';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { OpModalService } from 'core-app/shared/components/modal/modal.service';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
@Component({
template: `
<op-single-date-picker
tabindex="-1"
(changed)="onValueSelected($event)"
(canceled)="onCancel()"
(blurred)="submit($event)"
(enterPressed)="submit($event)"
[initialDate]="formatter(value)"
[required]="required"
[disabled]="inFlight"
[id]="handler.htmlId"
classes="inline-edit--field">
</op-single-date-picker>
[(ngModel)]="value"
[id]="handler.htmlId"
classes="inline-edit--field"
></op-single-date-picker>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DateEditFieldComponent extends EditFieldComponent implements OnInit {
@InjectField() readonly timezoneService:TimezoneService;
@InjectField() opModalService:OpModalService;
ngOnInit():void {
super.ngOnInit();
}
public onValueSelected(data:string):void {
this.value = this.parser(data);
public get value() {
return this.formatter(this.resource[this.name]) || '';
}
public submit(data:string):void {
this.onValueSelected(data);
public set value(value:any) {
this.resource[this.name] = this.parseValue(value);
void this.handler.handleUserSubmit();
}
@ -71,13 +62,6 @@ export class DateEditFieldComponent extends EditFieldComponent implements OnInit
this.handler.handleUserCancel();
}
public parser(data:string):string|null {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
return data;
}
return null;
}
public formatter(data:string):string|null {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
const d = this.timezoneService.parseDate(data);

@ -1,16 +1,15 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DateEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/date-edit-field/date-edit-field.component';
import { DatePickerModule } from 'core-app/shared/components/op-date-picker/date-picker.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
@NgModule({
declarations: [
DateEditFieldComponent,
],
imports: [
CommonModule,
DatePickerModule,
OpSharedModule,
],
declarations: [
DateEditFieldComponent,
],
exports: [
DateEditFieldComponent,

@ -30,26 +30,20 @@ import {
Directive,
OnDestroy,
OnInit,
Injector,
} from '@angular/core';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { OpModalService } from 'core-app/shared/components/modal/modal.service';
import { take } from 'rxjs/operators';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { EditFieldComponent } from 'core-app/shared/components/fields/edit/edit-field.component';
import { SingleDateModalComponent } from 'core-app/shared/components/datepicker/single-date-modal/single-date.modal';
import { MultiDateModalComponent } from 'core-app/shared/components/datepicker/multi-date-modal/multi-date.modal';
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
import { DeviceService } from 'core-app/core/browser/device.service';
@Directive()
export abstract class DatePickerEditFieldComponent extends EditFieldComponent implements OnInit, OnDestroy {
@InjectField() readonly timezoneService:TimezoneService;
@InjectField() opModalService:OpModalService;
@InjectField() deviceService:DeviceService;
protected modal:SingleDateModalComponent|MultiDateModalComponent|null = null;
@InjectField() injector:Injector;
ngOnInit():void {
super.ngOnInit();
@ -66,34 +60,9 @@ export abstract class DatePickerEditFieldComponent extends EditFieldComponent im
ngOnDestroy():void {
super.ngOnDestroy();
this.modal?.closeMe();
}
public showDatePickerModal():void {
const component = this.change.schema.isMilestone ? SingleDateModalComponent : MultiDateModalComponent;
this.opModalService.show<SingleDateModalComponent|MultiDateModalComponent>(
component,
this.injector,
{ changeset: this.change, fieldName: this.name },
!this.deviceService.isMobile,
).subscribe((modal) => {
this.modal = modal;
setTimeout(() => {
const modalElement = jQuery(modal.elementRef.nativeElement).find('.op-datepicker-modal');
const field = jQuery(this.elementRef.nativeElement);
modal.reposition(modalElement, field);
});
(modal as OpModalComponent)
.closingEvent
.pipe(take(1))
.subscribe(() => {
this.modal = null;
this.onModalClosed();
});
});
}
public showDatePickerModal():void { }
protected onModalClosed():void {
void this.handler.handleUserSubmit();

@ -0,0 +1,24 @@
<spot-drop-modal
[open]="opened"
(closed)="cancel()"
alignment="bottom-center"
>
<input
type="number"
slot="trigger"
class="inline-edit--field op-input"
[ngModel]="formattedValue"
(click)="onInputClick($event)"
(focus)="opened = true"
disabled="disabled"
[id]="handler.htmlId"
/>
<op-wp-multi-date-form
[changeset]="change"
[fieldName]="name"
(save)="save()"
(cancel)="cancel()"
slot="body"
></op-wp-multi-date-form>
</spot-drop-modal>

@ -33,17 +33,33 @@ import {
import { DatePickerEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/date-picker-edit-field.component';
@Component({
template: `
<input type="number"
class="inline-edit--field op-input"
[ngModel]="formattedValue"
disabled="disabled"
[id]="handler.htmlId" />
`,
templateUrl: './days-duration-edit-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DaysDurationEditFieldComponent extends DatePickerEditFieldComponent {
opened = false;
public get formattedValue():number {
return Number(moment.duration(this.value).asDays().toFixed(0));
}
ngOnInit():void {
super.ngOnInit();
}
onInputClick(event:MouseEvent) {
event.stopPropagation();
}
showDatePickerModal() {
this.opened = true;
}
save() {
this.onModalClosed();
}
cancel():void {
this.handler.reset();
}
}

@ -1,10 +1,12 @@
<input type="text"
class="inline-edit--field op-input"
[opAutofocus]="shouldFocus"
[attr.aria-required]="required"
[attr.required]="required"
[disabled]="inFlight"
[(ngModel)]="value"
(keydown)="handler.handleUserKeydown($event)"
(focusout)="handler.onFocusOut()"
[id]="handler.htmlId" />
<input
type="text"
class="inline-edit--field op-input some-class"
[opAutofocus]="shouldFocus"
[attr.aria-required]="required"
[attr.required]="required"
[disabled]="inFlight"
[(ngModel)]="value"
(keydown)="handler.handleUserKeydown($event)"
(focusout)="handler.onFocusOut()"
[id]="handler.htmlId"
/>

@ -1,18 +1,22 @@
<div [ngClass]="[
editFieldContainerClass,
fieldName,
active && '-active' || '',
wrapperClasses || '-small'
]"
(dragover)="startDragActivation($event)">
<div
[ngClass]="[
editFieldContainerClass,
fieldName,
active && '-active' || '',
wrapperClasses || '-small'
]"
(dragover)="startDragActivation($event)"
>
<div
#editContainer
[hidden]="!active"
></div>
<div #editContainer
[hidden]="!active">
</div>
<div (click)="activateIfEditable($event)"
(keydown.enter)="activateIfEditable($event)"
[hidden]="active"
tabindex="-1"
#displayContainer></div>
<div
(click)="activateIfEditable($event)"
(keydown.enter)="activateIfEditable($event)"
[hidden]="active"
tabindex="-1"
#displayContainer
></div>
</div>

@ -87,7 +87,8 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
public destroyed = false;
constructor(protected states:States,
constructor(
protected states:States,
protected injector:Injector,
protected elementRef:ElementRef,
protected opContextMenu:OPContextMenuService,
@ -96,7 +97,8 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
// Get parent field group from injector if we're in a form
@Optional() protected editForm:EditFormComponent,
protected cdRef:ChangeDetectorRef,
protected I18n:I18nService) {
protected I18n:I18nService,
) {
super();
}

@ -31,7 +31,8 @@ import { CommonModule } from '@angular/common';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { OpenprojectEditorModule } from 'core-app/shared/components/editor/openproject-editor.module';
import { OpenprojectAttachmentsModule } from 'core-app/shared/components/attachments/openproject-attachments.module';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpSpotModule } from 'core-app/spot/spot.module';
import { AttributeHelpTextModule } from 'core-app/shared/components/attribute-help-texts/attribute-help-text.module';
import { EditFieldService } from 'core-app/shared/components/fields/edit/edit-field.service';
import { DisplayFieldService } from 'core-app/shared/components/fields/display/display-field.service';
@ -62,12 +63,14 @@ import { EditFieldControlsModule } from 'core-app/shared/components/fields/edit/
import { ProjectEditFieldComponent } from './edit/field-types/project-edit-field.component';
import { HoursDurationEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/hours-duration-edit-field.component';
import { UserEditFieldComponent } from './edit/field-types/user-edit-field.component';
import { DaysDurationEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/days-duration-edit-field.compontent';
import { DaysDurationEditFieldComponent } from 'core-app/shared/components/fields/edit/field-types/days-duration-edit-field.component';
import { CombinedDateEditFieldComponent } from './edit/field-types/combined-date-edit-field.component';
@NgModule({
imports: [
CommonModule,
OPSharedModule,
OpSharedModule,
OpSpotModule,
OpenprojectAttachmentsModule,
OpenprojectEditorModule,
OpenprojectModalModule,
@ -108,6 +111,7 @@ import { DaysDurationEditFieldComponent } from 'core-app/shared/components/field
FloatEditFieldComponent,
PlainFormattableEditFieldComponent,
MultiSelectEditFieldComponent,
CombinedDateEditFieldComponent,
ProjectEditFieldComponent,
UserEditFieldComponent,
WorkPackageEditFieldComponent,

@ -29,7 +29,7 @@
import { Injector, NgModule } from '@angular/core';
import { DynamicModule } from 'ng-dynamic-component';
import { HookService } from 'core-app/features/plugins/hook-service';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { OpenprojectCalendarModule } from 'core-app/features/calendar/openproject-calendar.module';
import { FormsModule } from '@angular/forms';
@ -73,7 +73,7 @@ import { TimeEntriesCurrentUserConfigurationModalComponent } from './widgets/tim
FormsModule,
DragDropModule,
OPSharedModule,
OpSharedModule,
OpenprojectModalModule,
OpenprojectWorkPackagesModule,
OpenprojectWorkPackageGraphsModule,

@ -19,9 +19,10 @@
[attributeScope]="'Project'"></attribute-help-text>
</div>
<div class="attributes-map--value">
<op-editable-attribute-field [resource]="project"
[fieldName]="cf.key">
</op-editable-attribute-field>
<op-editable-attribute-field
[resource]="project"
[fieldName]="cf.key"
></op-editable-attribute-field>
</div>
</ng-container>
</div>

@ -1,5 +1,6 @@
<spot-drop-modal
[open]="dropModalOpen"
[allowRepositioning]="false"
(closed)="close()"
alignment="bottom-left"
class="op-project-list-modal"

@ -14,13 +14,32 @@
<span class="op-confirm-dialog--text spot-body-small"
[textContent]="text.text"
></span>
<span
*ngFor="let data of passedData"
class="spot-body-small"
>
<br/>
<strong>{{data}}</strong>
</span>
<ng-container *ngIf="showListData; else showData">
<span class="op-confirm-dialog--text spot-body-small"
[textContent]="listTitle"
></span>
<ul class="op-confirm-dialog--list">
<li *ngFor="let data of passedData">
<span>{{data}}</span>
</li>
</ul>
</ng-container>
<ng-template #showData>
<span
*ngFor="let data of passedData"
class="spot-body-small"
>
<br/>
<strong>{{data}}</strong>
</span>
</ng-template>
<span class="op-confirm-dialog--text spot-body-small"
[textContent]="warningText"
></span>
</div>
<div class="spot-action-bar">

@ -1,3 +1,6 @@
.op-confirm-dialog
&--text
white-space: pre-wrap
&--list
margin-left: var(--list-side-margin) !important

@ -53,6 +53,9 @@ export interface ConfirmDialogOptions {
closeByEscape?:boolean;
showClose?:boolean;
closeByDocument?:boolean;
showListData?:boolean;
listTitle?:string;
warningText?:string;
passedData?:string[];
dangerHighlighting?:boolean;
divideContent?:boolean;
@ -66,6 +69,12 @@ export interface ConfirmDialogOptions {
export class ConfirmDialogModalComponent extends OpModalComponent {
public showClose:boolean;
public showListData:boolean;
public listTitle:string;
public warningText:string;
public divideContent:boolean;
public confirmed = false;
@ -97,6 +106,9 @@ export class ConfirmDialogModalComponent extends OpModalComponent {
this.options = (locals.options || {}) as ConfirmDialogOptions;
this.dangerHighlighting = _.defaultTo(this.options.dangerHighlighting, false);
this.showListData = _.defaultTo(this.options.showListData, false);
this.listTitle = _.defaultTo(this.options.listTitle, '');
this.warningText = _.defaultTo(this.options.warningText, '');
this.passedData = _.defaultTo(this.options.passedData, []);
this.showClose = _.defaultTo(this.options.showClose, true);
this.divideContent = _.defaultTo(this.options.divideContent, false);

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

Loading…
Cancel
Save