Compare commits

...

56 Commits

Author SHA1 Message Date
Benjamin Bädorf 6ce6cfaa06
Fix double setting _opened in drop modal component 2 years ago
Benjamin Bädorf 7e89961485
Non-wp datepickers and other drop modals work 2 years ago
Benjamin Bädorf 8c1ec9600b
Drop modal teleportation seems to work 2 years ago
Benjamin Bädorf 72a8976ab5
Drop-modal with portal 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
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
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
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
Benjamin Bädorf 7c346b2e82
Change detection problems 2 years ago
Benjamin Bädorf f0d86c1cc2
Separate changeset and non-changeset datepickers 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
Benjamin Bädorf 3cc9c05748
Start transforming multi date modal 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
Benjamin Bädorf 3fc36bda26
Fix flatpickr initialization 2 years ago
Benjamin Bädorf eb165e37b7
Merge branch 'dev' into feature/42358-standardise-date-pickers 2 years ago
Benjamin Bädorf 53aac04610
Merge branch 'dev' into feature/42358-standardise-date-pickers 2 years ago
Benjamin Bädorf 8d5ec86b1c
Fix closing of single date picker 2 years ago
Benjamin Bädorf 9b63226795
Merge branch 'dev' into feature/42358-standardise-date-pickers 2 years ago
Benjamin Bädorf c8bfaf98c1
Start rewriting op-single-date-picker from scratch 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
Benjamin Bädorf 2ae17f10fb
Remove date-picker-control, use op-single-date-picker directly 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
  1. 2
      app/helpers/application_helper.rb
  2. 15
      app/helpers/custom_fields_helper.rb
  3. 2
      app/views/announcements/edit.html.erb
  4. 1
      app/views/layouts/base.html.erb
  5. 16
      app/views/projects/filters/date/_between_dates.html.erb
  6. 12
      app/views/projects/filters/date/_on_date.html.erb
  7. 16
      app/views/versions/_form.html.erb
  8. 20
      app/views/work_packages/bulk/edit.html.erb
  9. 20
      app/views/work_packages/moves/new.html.erb
  10. 4
      frontend/src/app/app.module.ts
  11. 4
      frontend/src/app/core/apiv3/openproject-api-v3.module.ts
  12. 4
      frontend/src/app/core/global_search/openproject-global-search.module.ts
  13. 12
      frontend/src/app/core/setup/global-dynamic-components.const.ts
  14. 4
      frontend/src/app/core/setup/globals/global-listeners.ts
  15. 31
      frontend/src/app/core/setup/globals/global-listeners/augmented-date-picker.ts
  16. 4
      frontend/src/app/features/admin/openproject-admin.module.ts
  17. 14
      frontend/src/app/features/backlogs/backlogs-page/styles/master_backlog.sass
  18. 4
      frontend/src/app/features/bim/bcf/openproject-bcf.module.ts
  19. 4
      frontend/src/app/features/bim/ifc_models/openproject-ifc-models.module.ts
  20. 4
      frontend/src/app/features/boards/openproject-boards.module.ts
  21. 4
      frontend/src/app/features/calendar/openproject-calendar.module.ts
  22. 4
      frontend/src/app/features/dashboards/openproject-dashboards.module.ts
  23. 4
      frontend/src/app/features/enterprise/openproject-enterprise.module.ts
  24. 4
      frontend/src/app/features/in-app-notifications/in-app-notifications.module.ts
  25. 4
      frontend/src/app/features/invite-user-modal/invite-user-modal.module.ts
  26. 4
      frontend/src/app/features/job-status/openproject-job-status.module.ts
  27. 3
      frontend/src/app/features/my-page/my-page.component.ts
  28. 4
      frontend/src/app/features/my-page/openproject-my-page.module.ts
  29. 4
      frontend/src/app/features/overview/openproject-overview.module.ts
  30. 4
      frontend/src/app/features/projects/openproject-projects.module.ts
  31. 4
      frontend/src/app/features/team-planner/team-planner/team-planner.module.ts
  32. 12
      frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.html
  33. 4
      frontend/src/app/features/user-preferences/user-preferences.module.ts
  34. 36
      frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.html
  35. 12
      frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts
  36. 40
      frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.html
  37. 35
      frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.ts
  38. 20
      frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.html
  39. 6
      frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.ts
  40. 29
      frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.html
  41. 25
      frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.ts
  42. 33
      frontend/src/app/features/work-packages/components/wp-activity/activity-entry.component.html
  43. 7
      frontend/src/app/features/work-packages/components/wp-activity/activity-entry.component.ts
  44. 4
      frontend/src/app/features/work-packages/components/wp-fast-table/builders/cell-builder.ts
  45. 199
      frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.html
  46. 8
      frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts
  47. 165
      frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.html
  48. 14
      frontend/src/app/features/work-packages/openproject-work-packages.module.ts
  49. 4
      frontend/src/app/shared/components/autocompleter/members-autocompleter/members.module.ts
  50. 10
      frontend/src/app/shared/components/datepicker/banner/datepicker-banner.component.ts
  51. 1
      frontend/src/app/shared/components/datepicker/constants.ts
  52. 58
      frontend/src/app/shared/components/datepicker/datepicker.ts
  53. 3
      frontend/src/app/shared/components/datepicker/helpers/date-modal.helpers.ts
  54. 113
      frontend/src/app/shared/components/datepicker/multi-date-picker/multi-date-picker.component.html
  55. 447
      frontend/src/app/shared/components/datepicker/multi-date-picker/multi-date-picker.component.ts
  56. 4
      frontend/src/app/shared/components/datepicker/scheduling-mode/datepicker-scheduling-toggle.component.ts
  57. 20
      frontend/src/app/shared/components/datepicker/services/date-modal-relations.service.ts
  58. 13
      frontend/src/app/shared/components/datepicker/services/date-modal-scheduling.service.ts
  59. 74
      frontend/src/app/shared/components/datepicker/single-date-modal/single-date.modal.html
  60. 93
      frontend/src/app/shared/components/datepicker/single-date-picker/single-date-picker.component.html
  61. 262
      frontend/src/app/shared/components/datepicker/single-date-picker/single-date-picker.component.ts
  62. 17
      frontend/src/app/shared/components/datepicker/styles/datepicker.modal.sass
  63. 4
      frontend/src/app/shared/components/datepicker/toggle/datepicker-working-days-toggle.component.ts
  64. 14
      frontend/src/app/shared/components/datepicker/wp-multi-date-form/wp-multi-date-form.component.html
  65. 185
      frontend/src/app/shared/components/datepicker/wp-multi-date-form/wp-multi-date-form.component.ts
  66. 70
      frontend/src/app/shared/components/datepicker/wp-single-date-form/wp-single-date-form.component.html
  67. 168
      frontend/src/app/shared/components/datepicker/wp-single-date-form/wp-single-date-form.component.ts
  68. 81
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-adapter/date-picker-adapter.component.ts
  69. 91
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-control/date-picker-control.component.ts
  70. 18
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-control/date-picker-control.module.ts
  71. 7
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/date-input.component.html
  72. 8
      frontend/src/app/shared/components/dynamic-forms/dynamic-forms.module.ts
  73. 2
      frontend/src/app/shared/components/fields/edit/edit-field.initializer.ts
  74. 4
      frontend/src/app/shared/components/fields/edit/edit-field.service.ts
  75. 4
      frontend/src/app/shared/components/fields/edit/field-controls/edit-field-controls.module.ts
  76. 32
      frontend/src/app/shared/components/fields/edit/field-types/combined-date-edit-field.component.html
  77. 39
      frontend/src/app/shared/components/fields/edit/field-types/combined-date-edit-field.component.ts
  78. 36
      frontend/src/app/shared/components/fields/edit/field-types/date-edit-field/date-edit-field.component.ts
  79. 11
      frontend/src/app/shared/components/fields/edit/field-types/date-edit-field/date-edit-field.module.ts
  80. 41
      frontend/src/app/shared/components/fields/edit/field-types/date-picker-edit-field.component.ts
  81. 24
      frontend/src/app/shared/components/fields/edit/field-types/days-duration-edit-field.component.html
  82. 31
      frontend/src/app/shared/components/fields/edit/field-types/days-duration-edit-field.component.ts
  83. 22
      frontend/src/app/shared/components/fields/edit/field-types/text-edit-field.component.html
  84. 36
      frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.html
  85. 6
      frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts
  86. 10
      frontend/src/app/shared/components/fields/openproject-fields.module.ts
  87. 4
      frontend/src/app/shared/components/grids/openproject-grids.module.ts
  88. 7
      frontend/src/app/shared/components/grids/widgets/project-details/project-details.component.html
  89. 3
      frontend/src/app/shared/components/header-project-select/header-project-select.component.html
  90. 127
      frontend/src/app/shared/components/op-date-picker/date-picker.directive.ts
  91. 19
      frontend/src/app/shared/components/op-date-picker/date-picker.module.ts
  92. 15
      frontend/src/app/shared/components/op-date-picker/op-range-date-picker/op-range-date-picker.component.html
  93. 84
      frontend/src/app/shared/components/op-date-picker/op-range-date-picker/op-range-date-picker.component.ts
  94. 16
      frontend/src/app/shared/components/op-date-picker/op-single-date-picker/op-single-date-picker.component.html
  95. 117
      frontend/src/app/shared/components/op-date-picker/op-single-date-picker/op-single-date-picker.component.ts
  96. 2
      frontend/src/app/shared/components/project-include/project-include.component.html
  97. 4
      frontend/src/app/shared/components/storages/openproject-storages.module.ts
  98. 5
      frontend/src/app/shared/components/time_entries/form/form.component.html
  99. 4
      frontend/src/app/shared/components/time_entries/openproject-time-entries.module.ts
  100. 58
      frontend/src/app/shared/components/time_entries/shared/modal/base.modal.html
  101. Some files were not shown because too many files have changed in this diff Show More

@ -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

@ -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'

@ -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) %>

@ -60,6 +60,7 @@ See COPYRIGHT and LICENSE files for more details.
</noscript>
<op-toasts-container></op-toasts-container>
<op-modal-overlay></op-modal-overlay>
<spot-drop-modal-portal></spot-drop-modal-portal>
<% main_menu = render_main_menu(local_assigns.fetch(:menu_name, nil), @project) %>
<% side_displayed = content_for?(:sidebar) || content_for?(:main_menu) || !main_menu.blank? %>
<% initial_classes = initial_menu_classes(side_displayed, show_decoration) %>

@ -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 %>

@ -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,
],

@ -203,7 +203,15 @@ 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';
import { SpotDropModalPortalComponent, spotDropModalPortalComponentSelector } from 'core-app/spot/components/drop-modal/drop-modal-portal.component';
export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: appBaseSelector, cls: ApplicationBaseComponent },
@ -261,4 +269,6 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: opInAppNotificationBellSelector, cls: InAppNotificationBellComponent },
{ selector: ianMenuSelector, cls: IanMenuComponent },
{ selector: opModalOverlaySelector, cls: OpModalOverlayComponent },
{ selector: opSingleDatePickerSelector, cls: OpSingleDatePickerComponent },
{ selector: spotDropModalPortalComponentSelector, cls: SpotDropModalPortalComponent },
];

@ -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
[opened]="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,93 @@
<spot-drop-modal
[opened]="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>
<ng-container slot="body">
<form
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"
>
<spot-text-field
slot="input"
name="date"
class="op-datepicker-modal--date-field"
[ngModel]="workingValue"
(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>
</ng-container>
</spot-drop-modal>
<input
[id]="id"
[name]="name"
[value]="value"
hidden
>

@ -0,0 +1,262 @@
// -- 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';
import { debounce } from 'lodash';
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.initializeDatepickerDebounced();
} 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:Event) {
$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.initializeDatepickerDebounced();
this.cdRef.detectChanges();
}
private enforceManualChangesToDatepicker(enforceDate?:Date) {
const date = parseDate(this.workingDate || '');
setDates(date, this.datePickerInstance, enforceDate);
}
private initializeDatepickerDebounced = debounce(this.initializeDatepicker.bind(this), 16);
private initializeDatepicker(numberOfTries = 0) {
this.datePickerInstance?.destroy();
// If we're too early somehow, try again in a bit
if (!this.flatpickrTarget?.nativeElement) {
if (numberOfTries >= 3) {
console.warn('Tried initializing flatpickr 3 times in a row with no success. Bailing out');
return;
}
this.initializeDatepickerDebounced(numberOfTries + 1);
}
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,4 @@
<form
class="spot-modal op-datepicker-modal loading-indicator--location"
data-qa-selector="op-datepicker-modal"
[attr.id]="htmlId"
#modalContainer
@ -8,11 +7,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-container op-datepicker-modal op-datepicker-modal_wide">
<div class="op-datepicker-modal--toggle-actions-container">
<op-datepicker-scheduling-toggle
name="scheduleManually"
@ -104,15 +103,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,26 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
}
ngAfterViewInit():void {
console.log(this.modalContainer);
debugger;
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 +337,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 +352,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 +387,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 +473,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 +505,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 +532,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 +734,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
[opened]="opened"
(closed)="cancel()"
alignment="bottom-center"
>
<input
slot="trigger"
type="text"
class="spot-input"
(click)="onInputClick($event)"
[value]="dates"
(focus)="showDatePickerModal()"
/>
<ng-container slot="body">
<op-wp-single-date-form
*ngIf="opened && !isMultiDate"
[value]="dates"
[changeset]="change"
(save)="save()"
(cancel)="cancel()"
></op-wp-single-date-form>
<op-wp-multi-date-form
*ngIf="opened && isMultiDate"
[value]="dates"
[changeset]="change"
[fieldName]="name"
(save)="save()"
(cancel)="cancel()"
></op-wp-multi-date-form>
</ng-container>
</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,36 +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();
}
protected onModalClosed():void { }
}

@ -0,0 +1,24 @@
<spot-drop-modal
[opened]="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,34 @@ 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.handler.handleUserSubmit();
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"
[opened]="dropModalOpen"
[allowRepositioning]="false"
(closed)="close()"
alignment="bottom-left"
class="op-project-list-modal"

@ -1,127 +0,0 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 {
AfterViewInit,
ChangeDetectorRef,
Directive,
ElementRef,
EventEmitter,
Injector,
Input,
NgZone,
OnDestroy,
Output,
ViewChild,
} from '@angular/core';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { DatePicker } from 'core-app/shared/components/op-date-picker/datepicker';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
@Directive()
export abstract class AbstractDatePickerDirective extends UntilDestroyedMixin implements OnDestroy, AfterViewInit {
@Output() public canceled = new EventEmitter<string>();
@Input() public appendTo?:HTMLElement;
@Input() public classes = '';
@Input() public id = '';
@Input() public name = '';
@Input() public required = false;
@Input() public size = 20;
@Input() public disabled = false;
@ViewChild('dateInput') dateInput:ElementRef;
protected datePickerInstance:DatePicker;
public constructor(
readonly injector:Injector,
protected timezoneService:TimezoneService,
protected configurationService:ConfigurationService,
protected ngZone:NgZone,
protected changeDetectorRef:ChangeDetectorRef,
) {
super();
if (!this.id) {
this.id = `datepicker-input-${Math.floor(Math.random() * 1000).toString(3)}`;
}
}
ngAfterViewInit():void {
this.initializeDatepicker();
}
ngOnDestroy():void {
if (this.datePickerInstance) {
this.datePickerInstance.destroy();
}
}
openOnClick():void {
if (!this.disabled) {
this.datePickerInstance.show();
}
}
closeOnOutsideClick(event:MouseEvent):void {
if (this.isOutsideClick(event)) {
this.close();
}
}
isOutsideClick(event:MouseEvent):boolean {
return (!(event.relatedTarget
&& this.datePickerInstance.datepickerInstance.calendarContainer.contains(event.relatedTarget as HTMLElement)));
}
close():void {
this.datePickerInstance.hide();
}
protected isEmpty():boolean {
return this.currentValue.trim() === '';
}
protected get currentValue():string {
return this.inputElement?.value || '';
}
protected get inputElement():HTMLInputElement {
return this.dateInput.nativeElement as HTMLInputElement;
}
protected abstract initializeDatepicker():void;
}

@ -1,19 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OpRangeDatePickerComponent } from 'core-app/shared/components/op-date-picker/op-range-date-picker/op-range-date-picker.component';
import { OpSingleDatePickerComponent } from 'core-app/shared/components/op-date-picker/op-single-date-picker/op-single-date-picker.component';
@NgModule({
declarations: [
OpSingleDatePickerComponent,
OpRangeDatePickerComponent,
],
imports: [
CommonModule,
],
exports: [
OpSingleDatePickerComponent,
OpRangeDatePickerComponent,
],
})
export class DatePickerModule { }

@ -1,15 +0,0 @@
<input
#dateInput
[id]="id"
[name]="name"
[value]="initialValue"
[ngClass]="classes + ' op-input'"
[size]="size"
[required]="required"
[disabled]="disabled"
(click)="openOnClick()"
(keydown.escape)="close()"
(keydown)="onKeyDown()"
(blur)="closeOnOutsideClick($event)"
type="text"
>

@ -1,84 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
Input,
Output,
} from '@angular/core';
import { Instance } from 'flatpickr/dist/types/instance';
import { KeyCodes } from 'core-app/shared/helpers/keyCodes.enum';
import { DatePicker } from 'core-app/shared/components/op-date-picker/datepicker';
import { AbstractDatePickerDirective } from 'core-app/shared/components/op-date-picker/date-picker.directive';
import { DebouncedEventEmitter } from 'core-app/shared/helpers/rxjs/debounced-event-emitter';
import { componentDestroyed } from '@w11k/ngx-componentdestroyed';
export const rangeSeparator = '-';
@Component({
selector: 'op-range-date-picker',
templateUrl: './op-range-date-picker.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OpRangeDatePickerComponent extends AbstractDatePickerDirective {
@Output() public changed = new DebouncedEventEmitter<string[]>(componentDestroyed(this));
@Input() public initialDates:string[] = [];
initialValue = '';
protected initializeDatepicker():void {
this.initialDates = this.initialDates || [];
this.initialValue = this.resolveDateArrayToString(this.initialDates);
const options = {
allowInput: true,
appendTo: this.appendTo,
mode: 'range' as const,
onChange: (selectedDates:Date[], dateStr:string) => {
if (this.isEmpty()) {
return;
}
this.inputElement.value = dateStr;
if (selectedDates.length === 2) {
this.changed.emit(this.resolveDateStringToArray(dateStr));
}
},
onKeyDown: (selectedDates:Date[], dateStr:string, instance:Instance, data:KeyboardEvent) => {
if (data.which === KeyCodes.ESCAPE) {
this.canceled.emit();
}
},
};
let initialValue;
if (this.isEmpty() && this.initialDates.length > 0) {
initialValue = this.initialDates.map((date) => this.timezoneService.parseISODate(date).toDate());
} else {
initialValue = this.resolveDateStringToArray(this.currentValue);
}
this.datePickerInstance = new DatePicker(
this.injector,
`#${this.id}`,
initialValue,
options,
null,
);
}
// eslint-disable-next-line class-methods-use-this
onKeyDown():boolean {
// Disable any manual user input as it most likely return in a wrong format
return false;
}
// eslint-disable-next-line class-methods-use-this
private resolveDateStringToArray(dates:string):string[] {
return dates.split(` ${rangeSeparator} `).map((date) => date.trim());
}
// eslint-disable-next-line class-methods-use-this
private resolveDateArrayToString(dates:string[]):string {
return dates.join(` ${rangeSeparator} `);
}
}

@ -1,16 +0,0 @@
<input
#dateInput
[id]="id"
[name]="name"
[value]="initialDate || ''"
[ngClass]="classes + ' op-input'"
[size]="size"
[required]="required"
[disabled]="disabled"
(click)="openOnClick()"
(keydown.enter)="enterPressed.emit(dateValue)"
(keydown.escape)="close()"
(input)="onInputChange()"
(blur)="onBlurred($event)"
type="text"
>

@ -1,117 +0,0 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Instance } from 'flatpickr/dist/types/instance';
import { KeyCodes } from 'core-app/shared/helpers/keyCodes.enum';
import { DatePicker } from 'core-app/shared/components/op-date-picker/datepicker';
import { AbstractDatePickerDirective } from 'core-app/shared/components/op-date-picker/date-picker.directive';
import { DebouncedEventEmitter } from 'core-app/shared/helpers/rxjs/debounced-event-emitter';
import { componentDestroyed } from '@w11k/ngx-componentdestroyed';
/* eslint-disable-next-line change-detection-strategy/on-push */
@Component({
selector: 'op-single-date-picker',
templateUrl: './op-single-date-picker.component.html',
})
export class OpSingleDatePickerComponent extends AbstractDatePickerDirective {
@Output() public changed = new DebouncedEventEmitter<string>(componentDestroyed(this));
@Output() public blurred = new EventEmitter<string>();
@Output() public enterPressed = new EventEmitter<string>();
@Input() public initialDate = '';
onInputChange():void {
if (this.inputIsValidDate()) {
this.changed.emit(this.currentValue);
}
}
onBlurred(event:MouseEvent):void {
if (this.isOutsideClick(event)) {
this.close();
this.blurred.emit(this.currentValue);
}
}
get dateValue():string {
if (this.inputIsValidDate()) {
return this.currentValue;
}
return '';
}
protected inputIsValidDate():boolean {
return (/\d{4}-\d{2}-\d{2}/.exec(this.currentValue)) !== null;
}
protected initializeDatepicker():void {
const options = {
allowInput: true,
appendTo: this.appendTo,
onChange: (selectedDates:Date[], dateStr:string) => {
const val:string = dateStr;
if (this.isEmpty()) {
return;
}
this.inputElement.value = val;
this.changed.emit(val);
},
onKeyDown: (selectedDates:Date[], dateStr:string, instance:Instance, data:KeyboardEvent) => {
if (data.which === KeyCodes.ESCAPE) {
this.canceled.emit();
}
},
};
let initialValue;
if (this.isEmpty() && this.initialDate) {
initialValue = this.timezoneService.parseISODate(this.initialDate).toDate();
} else {
initialValue = this.currentValue;
}
this.datePickerInstance = new DatePicker(
this.injector,
`#${this.id}`,
initialValue,
options,
null,
);
}
}

@ -1,5 +1,5 @@
<spot-drop-modal
[open]="opened"
[opened]="opened"
alignment="bottom-right"
(closed)="opened = false"
class="op-project-list-modal"

@ -43,7 +43,7 @@ import {
import {
FilePickerModalComponent,
} from 'core-app/shared/components/storages/file-picker-modal/file-picker-modal.component';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { SortFilesPipe } from 'core-app/shared/components/storages/pipes/sort-files.pipe';
import {
StorageFileListItemComponent,
@ -61,7 +61,7 @@ import { UploadStorageFilesService } from 'core-app/shared/components/storages/s
CommonModule,
IconModule,
OpSpotModule,
OPSharedModule,
OpSharedModule,
],
declarations: [
StorageComponent,

@ -2,7 +2,8 @@
#editForm
[resource]="entry"
[inEditMode]="inEditMode"
(onSaved)="signalModifiedEntry($event)">
(onSaved)="signalModifiedEntry($event)"
>
<div class="attributes-map">
<ng-container *ngIf="showUserField && schema.user.writable">
<div class="attributes-map--key"
@ -21,7 +22,7 @@
[ngClass]="{'-required': isRequired('spentOn')}"
[textContent]="schema.spentOn.name">
</div>
<div class="attributes-map--value">
<div class="attributes-map--value attributes-map--value_overflow">
<op-editable-attribute-field [resource]="entry"
[wrapperClasses]="'-tiny'"
[fieldName]="'spentOn'">

@ -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 { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openproject-fields.module';
import { TimeEntryCreateModalComponent } from 'core-app/shared/components/time_entries/create/create.modal';
@ -39,7 +39,7 @@ import { TriggerActionsEntryComponent } from 'core-app/shared/components/time_en
@NgModule({
imports: [
// Commons
OPSharedModule,
OpSharedModule,
OpenprojectModalModule,
// Editable fields e.g. for modals

@ -1,5 +1,5 @@
<div
class="spot-modal loading-indicator--location"
class="spot-modal spot-modal_allow-overflow loading-indicator--location"
data-indicator-name="modal"
>
<div class="spot-modal--header">
@ -9,38 +9,42 @@
<div class="spot-divider"></div>
<div class="ngdialog-body spot-modal--body spot-container">
<te-form #editForm
[changeset]="changeset"
[showWorkPackageField]="showWorkPackageField"
[showUserField]="showUserField"
(modifiedEntry)="setModifiedEntry($event)">
</te-form>
<div class="spot-modal--body spot-modal--body_no-scroll spot-container">
<te-form
#editForm
[changeset]="changeset"
[showWorkPackageField]="showWorkPackageField"
[showUserField]="showUserField"
(modifiedEntry)="setModifiedEntry($event)"
></te-form>
</div>
<div class="spot-action-bar">
<div class="spot-action-bar--left">
<button class="button button_no-margin -danger spot-action-bar--action"
*ngIf="deleteAllowed"
(click)="destroy()"
[textContent]="text.delete"
[attr.title]="text.delete">
</button>
<button
class="button button_no-margin -danger spot-action-bar--action"
*ngIf="deleteAllowed"
(click)="destroy()"
[textContent]="text.delete"
[attr.title]="text.delete"
></button>
</div>
<div class="spot-action-bar--right">
<button class="button button_no-margin spot-modal--cancel-button spot-action-bar--action"
*ngIf="saveAllowed"
(click)="closeMe($event)"
[textContent]="text.cancel"
[attr.title]="text.cancel">
</button>
<button class="button -highlight button_no-margin spot-action-bar--action"
*ngIf="saveAllowed"
(click)="saveEntry()"
[attr.title]="text.save"
[textContent]="text.save"
[disabled]="formInFlight">
</button>
<button
class="button button_no-margin spot-modal--cancel-button spot-action-bar--action"
*ngIf="saveAllowed"
(click)="closeMe($event)"
[textContent]="text.cancel"
[attr.title]="text.cancel"
></button>
<button
class="button -highlight button_no-margin spot-action-bar--action"
*ngIf="saveAllowed"
(click)="saveEntry()"
[attr.title]="text.save"
[textContent]="text.save"
[disabled]="formInFlight"
></button>
</div>
</div>
</div>

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

Loading…
Cancel
Save