Merge pull request #10669 from opf/implementation/42185-change-date-selection-logic-in-new-date-picker

Update flatpickr and remove most of the custom overwrites for the dat…
fix/42397-project-filter-is-not-applied-in-embedded-table
Henriette Darge 3 years ago committed by GitHub
commit 7eaca081e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      frontend/package-lock.json
  2. 2
      frontend/package.json
  3. 92
      frontend/src/app/shared/components/datepicker/datepicker.modal.helper.ts
  4. 10
      frontend/src/app/shared/components/datepicker/datepicker.modal.html
  5. 153
      frontend/src/app/shared/components/datepicker/datepicker.modal.ts
  6. 80
      spec/features/work_packages/details/date_editor_spec.rb
  7. 6
      spec/support/edit_fields/date_edit_field.rb

@ -9126,9 +9126,9 @@
}
},
"flatpickr": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.3.tgz",
"integrity": "sha512-007VucCkqNOMMb9ggRLNuJowwaJcyOh4sKAFcdGfahfGc7JQbf94zSzjdBq/wVyHWUEs5o3+idhFZ0wbZMRmVQ=="
"version": "4.6.13",
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw=="
},
"flatted": {
"version": "3.2.5",

@ -107,7 +107,7 @@
"dom-plane": "^1.0.2",
"dragula": "^3.5.2",
"expose-loader": "^0.7.5",
"flatpickr": "^4.6.3",
"flatpickr": "^4.6.13",
"fuse.js": "^3.4.5",
"glob": "^7.1.4",
"hammerjs": "^2.0.8",

@ -33,7 +33,7 @@ import { DateOption } from 'flatpickr/dist/types/options';
@Injectable({ providedIn: 'root' })
export class DatePickerModalHelper {
public currentlyActivatedDateField:DateKeys;
currentlyActivatedDateField:DateKeys;
/**
* Map the date to the internal format,
@ -62,10 +62,6 @@ export class DatePickerModalHelper {
|| !!new Date(date).valueOf();
}
sortDates(dates:Date[]):Date[] {
return dates.sort((a:Date, b:Date) => a.getTime() - b.getTime());
}
areDatesEqual(firstDate:Date|string, secondDate:Date|string):boolean {
const parsedDate1 = this.parseDate(firstDate);
const parsedDate2 = this.parseDate(secondDate);
@ -80,9 +76,8 @@ export class DatePickerModalHelper {
this.currentlyActivatedDateField = val;
}
toggleCurrentActivatedField(dates:{ [key in DateKeys]:string }, datePicker:DatePicker):void {
toggleCurrentActivatedField():void {
this.currentlyActivatedDateField = this.currentlyActivatedDateField === 'start' ? 'end' : 'start';
this.setDatepickerRestrictions(dates, datePicker);
}
isStateOfCurrentActivatedField(val:DateKeys):boolean {
@ -106,87 +101,4 @@ export class DatePickerModalHelper {
datePicker.datepickerInstance.redraw();
}
setDatepickerRestrictions(dates:{ [key in DateKeys]:string }, datePicker:DatePicker):void {
if (!dates.start && !dates.end) {
return;
}
let disableFunction:Function = (date:Date) => false;
if (this.isStateOfCurrentActivatedField('start') && dates.end) {
disableFunction = (date:Date) => date.getTime() > new Date(dates.end).setHours(0, 0, 0, 0);
} else if (this.isStateOfCurrentActivatedField('end') && dates.start) {
disableFunction = (date:Date) => date.getTime() < new Date(dates.start).setHours(0, 0, 0, 0);
}
datePicker.datepickerInstance.set('disable', [disableFunction]);
}
setRangeClasses(dates:{ [key in DateKeys]:string }):void {
if (!dates.start || !dates.end || (dates.start === dates.end)) {
return;
}
const monthContainer = document.getElementsByClassName('dayContainer');
// For each container of the two-month layout, set the highlighting classes
for (let i = 0; i < monthContainer.length; i++) {
this.highlightRangeInSingleMonth(monthContainer[i], dates);
}
}
private highlightRangeInSingleMonth(container:Element, dates:{ [key in DateKeys]:string }):void {
const selectedElements = jQuery(container).find('.flatpickr-day.selected');
if (selectedElements.length === 2) {
// Both dates are in the same month
selectedElements[0].classList.add('startRange');
selectedElements[1].classList.add('endRange');
this.selectRangeFromUntil(selectedElements[0], selectedElements[1]);
} else if (selectedElements.length === 1) {
// Only one date is in this month
if (DatePickerModalHelper.datepickerShowsDate(dates.start, selectedElements[0])) {
selectedElements[0].classList.add('startRange');
this.selectRangeFromUntil(selectedElements[0], '');
} else if (DatePickerModalHelper.datepickerShowsDate(dates.end, selectedElements[0])) {
const firstDay = jQuery(container).find('.flatpickr-day')[0];
selectedElements[0].classList.add('endRange');
firstDay.classList.add('inRange');
this.selectRangeFromUntil(firstDay, selectedElements[0]);
}
} else if (DatePickerModalHelper.datepickerIsInDateRange(container, dates)) {
// No date is in this month, but the month is completely between start and end date
jQuery(container).find('.flatpickr-day').addClass('inRange');
}
}
private static datepickerShowsDate(date:string, selectedElement:Element):boolean {
const isoDate = selectedElement.getAttribute('data-iso-date');
if (isoDate) {
return moment(isoDate).isSame(date, 'days');
}
return false;
}
private static datepickerIsInDateRange(container:Element, dates:{ [key in DateKeys]:string }):boolean {
const firstDayOfMonthElement = jQuery(container).find('.flatpickr-day:not(.hidden)')[0];
const isoDate = firstDayOfMonthElement.getAttribute('data-iso-date');
if (isoDate) {
const firstDayOfMonth = new Date(isoDate);
return firstDayOfMonth <= new Date(dates.end)
&& firstDayOfMonth >= new Date(dates.start);
}
return false;
}
private selectRangeFromUntil(from:Element, until:string|Element) {
jQuery(from).nextUntil(until).addClass('inRange');
}
}

@ -48,12 +48,13 @@
<div class="form--text-field-container -xslim">
<input type="text"
name="startDate"
data-qa-selector="op-datepicker-modal--start-date-field"
class="form--date-field"
[ngClass]="{'-current' : datepickerHelper.isStateOfCurrentActivatedField('start')}"
[ngModel]="dates.start"
(ngModelChange)="updateDate('start', $event)"
[disabled]="!isSchedulable"
(click)="setCurrentActivatedField('start')">
(focusin)="datepickerHelper.setCurrentActivatedField('start')">
</div>
<a class="form--field-inline-action"
*ngIf="isSchedulable"
@ -63,7 +64,7 @@
</a>
</div>
<div class="form--field-extra-actions">
<a *ngIf="showTodayLink('start')"
<a *ngIf="showTodayLink()"
(click)="setToday('start')"
[textContent]="text.today">
</a>
@ -79,12 +80,13 @@
<div class="form--text-field-container -xslim">
<input type="text"
name="endDate"
data-qa-selector="op-datepicker-modal--end-date-field"
class="form--date-field"
[ngClass]="{'-current' : datepickerHelper.isStateOfCurrentActivatedField('end')}"
[ngModel]="dates.end"
(ngModelChange)="updateDate('end', $event)"
[disabled]="!isSchedulable"
(click)="setCurrentActivatedField('end')">
(focusin)="datepickerHelper.setCurrentActivatedField('end')">
</div>
<a class="form--field-inline-action"
*ngIf="isSchedulable"
@ -94,7 +96,7 @@
</a>
</div>
<div class="form--field-extra-actions">
<a *ngIf="showTodayLink('end')"
<a *ngIf="showTodayLink()"
(click)="setToday('end')"
[textContent]="text.today">
</a>

@ -51,7 +51,7 @@ 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 { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { DayElement } from "flatpickr/dist/types/instance";
import { DayElement } from 'flatpickr/dist/types/instance';
import flatpickr from 'flatpickr';
export type DateKeys = 'date'|'start'|'end';
@ -89,15 +89,15 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
isSwitchedFromManualToAutomatic: this.I18n.t('js.work_packages.scheduling.is_switched_from_manual_to_automatic'),
};
public onDataUpdated = new EventEmitter<string>();
onDataUpdated = new EventEmitter<string>();
public singleDate = false;
singleDate = false;
public scheduleManually = false;
scheduleManually = false;
public htmlId = '';
htmlId = '';
public dates:{ [key in DateKeys]:string } = {
dates:{ [key in DateKeys]:string } = {
date: '',
start: '',
end: '',
@ -133,7 +133,7 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
ngAfterViewInit():void {
if (this.isSchedulable) {
this.showDateSelection();
this.initializeDatepicker();
}
this.onDataChange();
@ -144,7 +144,7 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
this.cdRef.detectChanges();
if (this.scheduleManually) {
this.showDateSelection();
this.initializeDatepicker();
} else if (this.isParent) {
this.removeDateSelection();
}
@ -212,20 +212,8 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
});
}
setCurrentActivatedField(key:DateKeys):void {
this.datepickerHelper.setCurrentActivatedField(key);
this.datepickerHelper.setDatepickerRestrictions(this.dates, this.datePickerInstance);
this.datepickerHelper.setRangeClasses(this.dates);
}
showTodayLink(key:DateKeys):boolean {
if (!this.isSchedulable) {
return false;
}
if (key === 'start') {
return !this.dates.end || this.datepickerHelper.parseDate(new Date()) <= this.datepickerHelper.parseDate(this.dates.end);
}
return !this.dates.start || this.datepickerHelper.parseDate(new Date()) >= this.datepickerHelper.parseDate(this.dates.start);
showTodayLink():boolean {
return this.isSchedulable;
}
/**
@ -256,12 +244,6 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
return !this.scheduleManually && !!this.changeset.value('scheduleManually');
}
private showDateSelection() {
this.initializeDatepicker();
this.datepickerHelper.setDatepickerRestrictions(this.dates, this.datePickerInstance);
this.datepickerHelper.setRangeClasses(this.dates);
}
private removeDateSelection() {
this.datePickerInstance.destroy();
}
@ -272,7 +254,7 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
'#flatpickr-input',
this.singleDate ? this.dates.date : [this.dates.start, this.dates.end],
{
mode: this.singleDate ? 'single' : 'multiple',
mode: this.singleDate ? 'single' : 'range',
showMonths: this.browserDetector.isMobile ? 1 : 2,
inline: true,
onChange: (dates:Date[]) => {
@ -280,12 +262,6 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
this.onDataChange();
},
onMonthChange: () => {
this.datepickerHelper.setRangeClasses(this.dates);
},
onYearChange: () => {
this.datepickerHelper.setRangeClasses(this.dates);
},
onDayCreate: (dObj:Date[], dStr:string, fp:flatpickr.Instance, dayElem:DayElement) => {
dayElem.setAttribute('data-iso-date', dayElem.dateObj.toISOString());
},
@ -303,76 +279,60 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
const dates = [this.datepickerHelper.parseDate(this.dates.start), this.datepickerHelper.parseDate(this.dates.end)];
this.datepickerHelper.setDates(dates, this.datePickerInstance, enforceDate);
this.setRangeClassesAndToggleActiveField(toggleField);
if (toggleField) {
this.datepickerHelper.toggleCurrentActivatedField();
}
}
}
private handleDatePickerChange(dates:Date[]) {
switch (dates.length) {
case 0: {
// In case we removed the only value by clicking on a already selected date within the datepicker:
if (this.dates.start || this.dates.end) {
this.setDateAndToggleActiveField(this.dates.start || this.dates.end);
}
break;
}
case 1: {
if (this.singleDate) {
this.dates.date = this.timezoneService.formattedISODate(dates[0]);
} else if (this.dates.start && this.dates.end) {
// Both dates are the same, so it is correct to only highlight one date
if (this.dates.start === this.dates.end) {
return;
}
// I wanted to set the new start date to the preselected endDate OR
// I wanted to set the new end date to the preselected startDate
if ((this.datepickerHelper.isStateOfCurrentActivatedField('start') && this.datepickerHelper.areDatesEqual(this.dates.start, dates[0]))
|| (this.datepickerHelper.isStateOfCurrentActivatedField('end') && this.datepickerHelper.areDatesEqual(this.dates.end, dates[0]))) {
const otherDateIndex:DateKeys = this.datepickerHelper.isStateOfCurrentActivatedField('start') ? 'end' : 'start';
this.setDateAndToggleActiveField(this.dates[otherDateIndex]);
const selectedDate = dates[0];
if (this.dates.start && this.dates.end) {
/**
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 = this.datepickerHelper.parseDate(this.dates.start) as Date;
const parsedEndDate = this.datepickerHelper.parseDate(this.dates.end) as Date;
if (selectedDate < parsedStartDate) {
this.overwriteDatePickerWithNewDates([selectedDate, parsedEndDate]);
this.datepickerHelper.setCurrentActivatedField('end');
} else if (selectedDate > parsedEndDate) {
if (this.datepickerHelper.isStateOfCurrentActivatedField('end')) {
this.overwriteDatePickerWithNewDates([parsedStartDate, selectedDate]);
} else {
this.dates.start = this.timezoneService.formattedISODate(selectedDate);
this.dates.end = '';
this.datepickerHelper.toggleCurrentActivatedField();
}
} else if (this.datepickerHelper.areDatesEqual(selectedDate, parsedStartDate) || this.datepickerHelper.areDatesEqual(selectedDate, parsedEndDate)) {
this.overwriteDatePickerWithNewDates([selectedDate, selectedDate]);
} else {
// I clicked on the already set start or end date (and thus removed it):
// We restore both values
this.enforceManualChangesToDatepicker(true);
const newDates = this.datepickerHelper.isStateOfCurrentActivatedField('start') ? [selectedDate, parsedEndDate] : [parsedStartDate, selectedDate];
this.overwriteDatePickerWithNewDates(newDates);
}
} else {
// It is the first value we set (either start or end date)
this.setDateAndToggleActiveField(this.timezoneService.formattedISODate(dates[0]), false);
this.dates[this.datepickerHelper.currentlyActivatedDateField] = this.timezoneService.formattedISODate(selectedDate);
}
break;
}
case 2: {
if ((!this.dates.end && this.datepickerHelper.isStateOfCurrentActivatedField('start'))
|| (!this.dates.start && this.datepickerHelper.isStateOfCurrentActivatedField('end'))) {
// If we change a start date when no end date is set, we keep only the newly clicked value and not both
this.overwriteDatePickerWithNewDates([dates[1]]);
} else {
// Sort dates so that the start date is always first
if (dates[0] > dates[1]) {
// eslint-disable-next-line no-param-reassign
dates = this.datepickerHelper.sortDates(dates);
this.datepickerHelper.setDates(dates, this.datePickerInstance);
}
const index = this.datepickerHelper.isStateOfCurrentActivatedField('start') ? 0 : 1;
this.dates[this.datepickerHelper.currentlyActivatedDateField] = this.timezoneService.formattedISODate(dates[index]);
this.setRangeClassesAndToggleActiveField();
}
// Write the dates to the input fields
this.dates.start = this.timezoneService.formattedISODate(dates[0]);
this.dates.end = this.timezoneService.formattedISODate(dates[1]);
this.datepickerHelper.toggleCurrentActivatedField();
break;
}
default: {
// Reset the date picker with the two new values
if (this.datepickerHelper.isStateOfCurrentActivatedField('start')) {
this.overwriteDatePickerWithNewDates([dates[2], dates[1]]);
} else {
this.overwriteDatePickerWithNewDates([dates[0], dates[2]]);
}
break;
}
}
@ -385,21 +345,6 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
this.handleDatePickerChange(dates);
}
private setDateAndToggleActiveField(newDate:string, forceDatePickerUpdate = true) {
this.dates[this.datepickerHelper.currentlyActivatedDateField] = newDate;
if (forceDatePickerUpdate) {
this.datepickerHelper.setDates([this.datepickerHelper.parseDate(newDate)], this.datePickerInstance);
}
this.datepickerHelper.toggleCurrentActivatedField(this.dates, this.datePickerInstance);
}
private setRangeClassesAndToggleActiveField(toggleField = true) {
if (toggleField) {
this.datepickerHelper.toggleCurrentActivatedField(this.dates, this.datePickerInstance);
}
this.datepickerHelper.setRangeClasses(this.dates);
}
private onDataChange() {
const date = this.dates.date || '';
const start = this.dates.start || '';
@ -410,6 +355,6 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
}
private initialActivatedField():DateKeys {
return this.locals.fieldName === 'dueDate' || (this.dates.start && !this.dates.end) ? 'end' : 'start';
return this.locals.fieldName === 'dueDate' ? 'end' : 'start';
}
}

@ -37,7 +37,7 @@ describe 'date inplace editor',
with_settings: { date_format: '%Y-%m-%d' },
js: true, selenium: true do
let(:project) { create :project_with_types, public: true }
let(:work_package) { create :work_package, project: project, start_date: '2016-01-01' }
let(:work_package) { create :work_package, project: project, start_date: '2016-01-02' }
let(:user) { create :admin }
let(:work_packages_page) { Pages::FullWorkPackage.new(work_package, project) }
let(:wp_table) { Pages::WorkPackagesTable.new(project) }
@ -61,7 +61,20 @@ describe 'date inplace editor',
start_date.save!
start_date.expect_inactive!
start_date.expect_state_text '2016-01-01 - 2016-01-25'
start_date.expect_state_text '2016-01-02 - 2016-01-25'
end
it 'reverses the dates if the selected date is before the current start date' do
start_date.activate!
start_date.expect_active!
start_date.datepicker.expect_year '2016'
start_date.datepicker.expect_month 'January', true
start_date.datepicker.select_day '1'
start_date.save!
start_date.expect_inactive!
start_date.expect_state_text '2016-01-01 - 2016-01-02'
end
it 'can set "today" as a date via the provided link' do
@ -79,6 +92,69 @@ describe 'date inplace editor',
start_date.expect_state_text "#{Time.zone.today.strftime('%Y-%m-%d')} - no finish date"
end
context 'with start and end date set' do
let(:work_package) { create :work_package, project: project, start_date: '2016-01-02', due_date: '2016-01-25' }
it 'selecting a date before the current start date will change the start date' do
start_date.activate!
start_date.expect_active!
start_date.datepicker.expect_year '2016'
start_date.datepicker.expect_month 'January', true
start_date.datepicker.select_day '1'
start_date.save!
start_date.expect_inactive!
start_date.expect_state_text '2016-01-01 - 2016-01-25'
end
it 'selecting a date in between changes the date that is currently in focus' do
start_date.activate!
start_date.expect_active!
start_date.datepicker.expect_year '2016'
start_date.datepicker.expect_month 'January', true
start_date.datepicker.select_day '3'
# Since the focus shifts automatically, we can directly click again to modify the end date
start_date.datepicker.select_day '21'
start_date.save!
start_date.expect_inactive!
start_date.expect_state_text '2016-01-03 - 2016-01-21'
end
it 'selecting a date after the current finish date will change either start or finish depending on the focus' do
start_date.activate!
start_date.expect_active!
start_date.datepicker.expect_year '2016'
start_date.datepicker.expect_month 'January', true
# Focus the end date field
start_date.activate_due_date_within_modal
start_date.datepicker.set_date '2016-03-01', true
# Since the end date is focused, the date will become the new end date
start_date.save!
start_date.expect_inactive!
start_date.expect_state_text '2016-01-02 - 2016-03-01'
# Activating again and now changing the start date to something after the current end date
start_date.activate!
start_date.expect_active!
start_date.datepicker.expect_year '2016'
start_date.datepicker.expect_month 'January', true
start_date.datepicker.set_date '2016-04-01', true
# This will set the new start and unset the end date
start_date.save!
start_date.expect_inactive!
start_date.expect_state_text '2016-04-01 - no finish date'
end
end
context 'with the start date empty' do
let(:work_package) { create :work_package, project: project, start_date: nil }

@ -56,6 +56,12 @@ class DateEditField < EditField
end
end
def activate_due_date_within_modal
within_modal do
find('[data-qa-selector="op-datepicker-modal--end-date-field"]').click
end
end
def modal_element
page.find(modal_selector)
end

Loading…
Cancel
Save