Merge pull request #10752 from opf/feature/non-working-days-frontend

Add weekdays to the datepicker instances
pull/10779/head
Oliver Günther 3 years ago committed by GitHub
commit 42da22b9f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      frontend/src/app/core/apiv3/api-v3.service.ts
  2. 33
      frontend/src/app/core/apiv3/endpoints/days/api-v3-day-paths.ts
  3. 50
      frontend/src/app/core/apiv3/endpoints/days/api-v3-days-paths.ts
  4. 2
      frontend/src/app/core/setup/globals/global-listeners/augmented-date-picker.ts
  5. 21
      frontend/src/app/core/state/collection-store.ts
  6. 9
      frontend/src/app/core/state/days/day.model.ts
  7. 54
      frontend/src/app/core/state/days/day.service.ts
  8. 19
      frontend/src/app/core/state/days/day.store.ts
  9. 9
      frontend/src/app/core/state/days/weekday.model.ts
  10. 67
      frontend/src/app/core/state/days/weekday.service.ts
  11. 19
      frontend/src/app/core/state/days/weekday.store.ts
  12. 4
      frontend/src/app/core/state/openproject-state.module.ts
  13. 8
      frontend/src/app/shared/components/datepicker/datepicker.modal.ts
  14. 9
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-adapter/date-picker-adapter.component.ts
  15. 39
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/date-input/components/date-picker-control/date-picker-control.component.ts
  16. 12
      frontend/src/app/shared/components/op-date-picker/date-picker.directive.ts
  17. 123
      frontend/src/app/shared/components/op-date-picker/datepicker.ts
  18. 2
      frontend/src/app/shared/components/op-date-picker/op-range-date-picker/op-range-date-picker.component.ts
  19. 3
      frontend/src/app/shared/components/op-date-picker/op-single-date-picker/op-single-date-picker.component.ts
  20. 4
      frontend/src/global_styles/content/_datepicker.sass
  21. 91
      spec/features/work_packages/details/workdays_spec.rb

@ -61,6 +61,7 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { ApiV3NotificationsPaths } from 'core-app/core/apiv3/endpoints/notifications/apiv3-notifications-paths';
import { ApiV3ViewsPaths } from 'core-app/core/apiv3/endpoints/views/apiv3-views-paths';
import { Apiv3BackupsPath } from 'core-app/core/apiv3/endpoints/backups/apiv3-backups-path';
import { ApiV3DaysPaths } from 'core-app/core/apiv3/endpoints/days/api-v3-days-paths';
@Injectable({ providedIn: 'root' })
export class ApiV3Service {
@ -73,6 +74,9 @@ export class ApiV3Service {
// /api/v3/configuration
public readonly configuration = this.apiV3CustomEndpoint(ApiV3ConfigurationPath);
// /api/v3/days
public readonly days = this.apiV3CustomEndpoint(ApiV3DaysPaths);
// /api/v3/documents
public readonly documents = this.apiV3CollectionEndpoint('documents');

@ -0,0 +1,33 @@
// -- 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 { ApiV3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { IDay } from 'core-app/core/state/days/day.model';
export class ApiV3DayPaths extends ApiV3GettableResource<IDay> {
}

@ -0,0 +1,50 @@
// -- 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 { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { ApiV3DayPaths } from 'core-app/core/apiv3/endpoints/days/api-v3-day-paths';
import { IDay } from 'core-app/core/state/days/day.model';
import {
ApiV3GettableResource,
ApiV3ResourceCollection,
} from 'core-app/core/apiv3/paths/apiv3-resource';
export class ApiV3DaysPaths extends ApiV3ResourceCollection<IDay, ApiV3DayPaths> {
// Base path
public readonly path:string;
constructor(readonly apiRoot:ApiV3Service,
protected basePath:string) {
super(apiRoot, basePath, 'days', ApiV3DayPaths);
}
// Static paths
// /api/v3/days/week
public readonly week = new ApiV3GettableResource(this.apiRoot, this.path, 'week', this);
}

@ -15,6 +15,7 @@ export function augmentedDatePicker(evt:JQuery.TriggeredEvent, target:JQuery) {
window.OpenProject.getPluginContext()
.then((context) => {
const datePicker = new DatePicker(
context.injector,
'.-augmented-datepicker',
target.val() as string,
{
@ -22,7 +23,6 @@ export function augmentedDatePicker(evt:JQuery.TriggeredEvent, target:JQuery) {
allowInput: true,
},
target[0],
context.services.configurationService,
);
datePicker.show();
})

@ -9,6 +9,8 @@ import {
ApiV3ListParameters,
listParamsString,
} from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { IHalResourceLinks } from 'core-app/core/state/hal-resource';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
export interface CollectionResponse {
ids:ID[];
@ -97,3 +99,22 @@ export function collectionFrom<T>(elements:T[]):IHALCollection<T> {
},
};
}
/**
* Takes a collection of elements that do not have an ID, and extract the ID from self link.
* @param collection a IHALCollection with elements that have a self link
* @returns the same collection with elements extended with an ID dervied from the self link.
*/
export function extendCollectionElementsWithId<T extends { _links:IHalResourceLinks }>(
collection:IHALCollection<T>,
):IHALCollection<T&{ id:ID }> {
const elements = collection._embedded.elements.map((element) => ({ ...element, id: idFromLink(element._links.self.href) }));
return {
...collection,
_embedded: {
...collection._embedded,
elements,
},
};
}

@ -0,0 +1,9 @@
import { IHalResourceLinks } from 'core-app/core/state/hal-resource';
export interface IDay {
id:string;
date:string;
name:string;
working:boolean;
_links:IHalResourceLinks;
}

@ -0,0 +1,54 @@
import { Injectable } from '@angular/core';
import {
map,
tap,
} from 'rxjs/operators';
import { Observable } from 'rxjs';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
import { HttpClient } from '@angular/common/http';
import { ApiV3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import {
collectionKey,
extendCollectionElementsWithId,
insertCollectionIntoState,
} from 'core-app/core/state/collection-store';
import { DayStore } from 'core-app/core/state/days/day.store';
import { IDay } from 'core-app/core/state/days/day.model';
import {
CollectionStore,
ResourceCollectionService,
} from 'core-app/core/state/resource-collection.service';
@Injectable()
export class DayResourceService extends ResourceCollectionService<IDay> {
private get daysPath():string {
return this
.apiV3Service
.days
.path;
}
constructor(
private http:HttpClient,
private apiV3Service:ApiV3Service,
) {
super();
}
fetchDays(params:ApiV3ListParameters):Observable<IHALCollection<IDay>> {
const collectionURL = collectionKey(params);
return this
.http
.get<IHALCollection<IDay>>(this.daysPath + collectionURL)
.pipe(
map((collection) => extendCollectionElementsWithId(collection)),
tap((collection) => insertCollectionIntoState(this.store, collection, collectionURL)),
);
}
protected createStore():CollectionStore<IDay> {
return new DayStore();
}
}

@ -0,0 +1,19 @@
import {
EntityStore,
StoreConfig,
} from '@datorama/akita';
import {
CollectionState,
createInitialCollectionState,
} from 'core-app/core/state/collection-store';
import { IDay } from 'core-app/core/state/days/day.model';
export interface DayState extends CollectionState<IDay> {
}
@StoreConfig({ name: 'days' })
export class DayStore extends EntityStore<DayState> {
constructor() {
super(createInitialCollectionState());
}
}

@ -0,0 +1,9 @@
import { IHalResourceLinks } from 'core-app/core/state/hal-resource';
export interface IWeekday {
id:string;
day:1|2|3|4|5|6|7;
name:string;
working:boolean;
_links:IHalResourceLinks;
}

@ -0,0 +1,67 @@
import { Injectable } from '@angular/core';
import {
map,
switchMap,
tap,
} from 'rxjs/operators';
import {
EMPTY,
Observable,
} from 'rxjs';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
import { HttpClient } from '@angular/common/http';
import {
extendCollectionElementsWithId,
insertCollectionIntoState,
} from 'core-app/core/state/collection-store';
import { WeekdayStore } from 'core-app/core/state/days/weekday.store';
import { IWeekday } from 'core-app/core/state/days/weekday.model';
import {
CollectionStore,
ResourceCollectionService,
} from 'core-app/core/state/resource-collection.service';
@Injectable()
export class WeekdayResourceService extends ResourceCollectionService<IWeekday> {
private get weekdaysPath():string {
return this
.apiV3Service
.days
.week
.path;
}
constructor(
private http:HttpClient,
private apiV3Service:ApiV3Service,
) {
super();
}
require():Observable<IWeekday[]> {
return this
.query
.selectHasCache()
.pipe(
switchMap((hasCache) => (hasCache ? EMPTY : this.fetchWeekdays())),
switchMap(() => this.query.selectAll()),
);
}
private fetchWeekdays():Observable<IHALCollection<IWeekday>> {
const collectionURL = 'all'; // We load all weekdays
return this
.http
.get<IHALCollection<IWeekday>>(this.weekdaysPath)
.pipe(
map((collection) => extendCollectionElementsWithId(collection)),
tap((collection) => insertCollectionIntoState(this.store, collection, collectionURL)),
);
}
protected createStore():CollectionStore<IWeekday> {
return new WeekdayStore();
}
}

@ -0,0 +1,19 @@
import {
EntityStore,
StoreConfig,
} from '@datorama/akita';
import {
CollectionState,
createInitialCollectionState,
} from 'core-app/core/state/collection-store';
import { IWeekday } from 'core-app/core/state/days/weekday.model';
export interface WeekdayState extends CollectionState<IWeekday> {
}
@StoreConfig({ name: 'weekdays' })
export class WeekdayStore extends EntityStore<WeekdayState> {
constructor() {
super(createInitialCollectionState());
}
}

@ -32,6 +32,8 @@ import { InAppNotificationsResourceService } from './in-app-notifications/in-app
import { ProjectsResourceService } from './projects/projects.service';
import { PrincipalsResourceService } from './principals/principals.service';
import { CapabilitiesResourceService } from 'core-app/core/state/capabilities/capabilities.service';
import { DayResourceService } from 'core-app/core/state/days/day.service';
import { WeekdayResourceService } from 'core-app/core/state/days/weekday.service';
@NgModule({
providers: [
@ -40,6 +42,8 @@ import { CapabilitiesResourceService } from 'core-app/core/state/capabilities/ca
ProjectsResourceService,
PrincipalsResourceService,
CapabilitiesResourceService,
DayResourceService,
WeekdayResourceService,
],
})
export class OpenProjectStateModule {}

@ -141,7 +141,7 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
changeSchedulingMode():void {
this.scheduleManually = !this.scheduleManually;
this.toggleDisabledState(this.datePickerInstance.datepickerInstance);
this.initializeDatepicker();
this.cdRef.detectChanges();
}
@ -228,6 +228,7 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
private initializeDatepicker() {
this.datePickerInstance?.destroy();
this.datePickerInstance = new DatePicker(
this.injector,
'#flatpickr-input',
this.singleDate ? this.dates.date : [this.dates.start, this.dates.end],
{
@ -243,11 +244,14 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
this.onDataChange();
},
onDayCreate: (dObj:Date[], dStr:string, fp:flatpickr.Instance, dayElem:DayElement) => {
if (this.datePickerInstance?.isDateDisabled(dayElem.dateObj)) {
dayElem.classList.add('flatpickr-non-working-day');
}
dayElem.setAttribute('data-iso-date', dayElem.dateObj.toISOString());
},
},
null,
this.configurationService,
);
}

@ -23,15 +23,6 @@ export class DatePickerAdapterComponent extends OpSingleDatePickerComponent impl
onControlTouch = () => { };
constructor(
timezoneService:TimezoneService,
configurationService:ConfigurationService,
private ngZone:NgZone,
private changeDetectorRef:ChangeDetectorRef,
) {
super(timezoneService, configurationService);
}
writeValue(date:string):void {
this.initialDate = this.formatter(date);
}

@ -1,12 +1,17 @@
import {
AfterViewInit, ChangeDetectorRef, Component, forwardRef, Input, NgZone,
AfterViewInit,
Component,
forwardRef,
Input,
} from '@angular/core';
import * as moment from 'moment';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
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';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
/* 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',
@ -20,30 +25,24 @@ import { ConfigurationService } from 'core-app/core/config/configuration.service
})
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;
onControlChange = (_:any) => { };
onControlTouch = () => { };
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
onControlChange:(_?:unknown) => void = () => { };
constructor(
timezoneService:TimezoneService,
configurationService:ConfigurationService,
private ngZone:NgZone,
private changeDetectorRef:ChangeDetectorRef,
) {
super(timezoneService, configurationService);
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
onControlTouch:(_?:unknown) => void = () => { };
writeValue(date:string):void {
this.initialDate = this.formatter(date);
}
registerOnChange(fn:(_:any) => void):void {
registerOnChange(fn:(_:unknown) => void):void {
this.onControlChange = fn;
}
registerOnTouched(fn:any):void {
registerOnTouched(fn:(_:unknown) => void):void {
this.onControlTouch = fn;
}
@ -69,19 +68,19 @@ export class DatePickerControlComponent extends OpSingleDatePickerComponent impl
this.onControlTouch();
}
closeOnOutsideClick(event:any) {
closeOnOutsideClick(event:MouseEvent):void {
super.closeOnOutsideClick(event);
this.onControlTouch();
}
public parser(data:any) {
public parser(data:string):string|null {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
return data;
}
return null;
}
public formatter(data:any):string {
public formatter(data:string):string {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
const d = this.timezoneService.parseDate(data);

@ -28,10 +28,13 @@
import {
AfterViewInit,
ChangeDetectorRef,
Directive,
ElementRef,
EventEmitter,
Injector,
Input,
NgZone,
OnDestroy,
Output,
ViewChild,
@ -64,8 +67,11 @@ export abstract class AbstractDatePickerDirective extends UntilDestroyedMixin im
protected datePickerInstance:DatePicker;
public constructor(
readonly injector:Injector,
protected timezoneService:TimezoneService,
protected configurationService:ConfigurationService,
protected ngZone:NgZone,
protected changeDetectorRef:ChangeDetectorRef,
) {
super();
@ -90,9 +96,9 @@ export abstract class AbstractDatePickerDirective extends UntilDestroyedMixin im
}
}
closeOnOutsideClick(event:any):void {
closeOnOutsideClick(event:MouseEvent):void {
if (!(event.relatedTarget
&& this.datePickerInstance.datepickerInstance.calendarContainer.contains(event.relatedTarget))) {
&& this.datePickerInstance.datepickerInstance.calendarContainer.contains(event.relatedTarget as HTMLElement))) {
this.close();
}
}
@ -110,7 +116,7 @@ export abstract class AbstractDatePickerDirective extends UntilDestroyedMixin im
}
protected get inputElement():HTMLInputElement {
return this.dateInput?.nativeElement;
return this.dateInput.nativeElement as HTMLInputElement;
}
protected abstract initializeDatepicker():void;

@ -25,57 +25,53 @@
//
// See COPYRIGHT and LICENSE files for more details.
//++
import * as moment from 'moment';
import flatpickr from 'flatpickr';
import { Instance } from 'flatpickr/dist/types/instance';
import {
DayElement,
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 { WeekdayResourceService } from 'core-app/core/state/days/weekday.service';
import { IWeekday } from 'core-app/core/state/days/weekday.model';
import { Injector } from '@angular/core';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { take } from 'rxjs/operators';
import DateOption = flatpickr.Options.DateOption;
export class DatePicker {
private datepickerFormat = 'Y-m-d';
private datepickerCont:HTMLElement = document.querySelector(this.datepickerElemIdentifier)!;
private datepickerCont:HTMLElement = document.querySelector(this.datepickerElemIdentifier) as HTMLElement;
public datepickerInstance:Instance;
private reshowTimeout:any;
private reshowTimeout:ReturnType<typeof setTimeout>;
private weekdays:IWeekday[] = [];
@InjectField() configurationService:ConfigurationService;
constructor(private datepickerElemIdentifier:string,
@InjectField() weekdaysService:WeekdayResourceService;
@InjectField() I18n:I18nService;
constructor(
readonly injector:Injector,
private datepickerElemIdentifier:string,
private date:Date|Date[]|string[]|string,
private options:flatpickr.Options.Options,
private datepickerTarget:HTMLElement|null,
private configurationService:ConfigurationService) {
this.initialize(options);
) {
void this.initialize(options);
}
private initialize(options:flatpickr.Options.Options) {
const I18n = new I18nService();
const firstDayOfWeek = this.configurationService.startOfWeek() as number;
this.loadWeekdays();
const mergedOptions = _.extend({}, options, {
weekNumbers: true,
getWeek(dateObj:Date) {
return moment(dateObj).format('W');
},
dateFormat: this.datepickerFormat,
defaultDate: this.date,
locale: {
weekdays: {
shorthand: I18n.t('date.abbr_day_names'),
longhand: I18n.t('date.day_names'),
},
months: {
shorthand: I18n.t<string[]>('date.abbr_month_names').slice(1),
longhand: I18n.t<string[]>('date.month_names').slice(1),
},
firstDayOfWeek,
weekAbbreviation: I18n.t('date.abbr_week'),
rangeSeparator: ` ${rangeSeparator} `,
},
});
const mergedOptions = _.extend({}, this.defaultOptions, options);
let datePickerInstances:Instance|Instance[];
if (this.datepickerTarget) {
@ -89,16 +85,16 @@ export class DatePicker {
document.addEventListener('scroll', this.hideDuringScroll, true);
}
public clear() {
public clear():void {
this.datepickerInstance.clear();
}
public destroy() {
public destroy():void {
this.hide();
this.datepickerInstance.destroy();
}
public hide() {
public hide():void {
if (this.isOpen) {
this.datepickerInstance.close();
}
@ -106,12 +102,12 @@ export class DatePicker {
document.removeEventListener('scroll', this.hideDuringScroll, true);
}
public show() {
public show():void {
this.datepickerInstance.open();
document.addEventListener('scroll', this.hideDuringScroll, true);
}
public setDates(dates:DateOption|DateOption[]) {
public setDates(dates:DateOption|DateOption[]):void {
this.datepickerInstance.setDate(dates);
}
@ -122,7 +118,7 @@ export class DatePicker {
private hideDuringScroll = (event:Event) => {
// Prevent Firefox quirk: flatPicker emits
// multiple scrolls event when it is open
const target = event.target! as HTMLInputElement;
const target = event.target as HTMLInputElement;
if (target?.classList?.contains('flatpickr-monthDropdown-months') || target?.classList?.contains('flatpickr-input')) {
return;
@ -146,12 +142,13 @@ export class DatePicker {
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) {
private isInViewport(element:HTMLElement):boolean {
const rect = element.getBoundingClientRect();
return (
@ -161,4 +158,56 @@ export class DatePicker {
&& rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
public isDateDisabled(date:Date):boolean {
const dayOfWeek = moment(date).isoWeekday();
return !!this.weekdays.find((wd) => wd.day === dayOfWeek && !wd.working);
}
private loadWeekdays() {
this
.weekdaysService
.require()
.pipe(
take(1),
)
.subscribe((weekdays) => {
this.weekdays = weekdays;
if (this.datepickerInstance) {
this.datepickerInstance.redraw();
}
});
}
private get defaultOptions() {
const firstDayOfWeek = this.configurationService.startOfWeek();
return {
weekNumbers: true,
getWeek(dateObj:Date) {
return moment(dateObj).format('W');
},
onDayCreate: (dObj:Date[], dStr:string, fp:flatpickr.Instance, dayElem:DayElement) => {
if (this.isDateDisabled(dayElem.dateObj)) {
dayElem.classList.add('flatpickr-grey-out');
}
},
dateFormat: this.datepickerFormat,
defaultDate: this.date,
locale: {
weekdays: {
shorthand: this.I18n.t('date.abbr_day_names'),
longhand: this.I18n.t('date.day_names'),
},
months: {
shorthand: this.I18n.t<string[]>('date.abbr_month_names').slice(1),
longhand: this.I18n.t<string[]>('date.month_names').slice(1),
},
firstDayOfWeek,
weekAbbreviation: this.I18n.t('date.abbr_week'),
rangeSeparator: ` ${rangeSeparator} `,
},
};
}
}

@ -58,11 +58,11 @@ export class OpRangeDatePickerComponent extends AbstractDatePickerDirective {
}
this.datePickerInstance = new DatePicker(
this.injector,
`#${this.id}`,
initialValue,
options,
null,
this.configurationService,
);
}

@ -34,6 +34,7 @@ import { AbstractDatePickerDirective } from 'core-app/shared/components/op-date-
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',
@ -84,11 +85,11 @@ export class OpSingleDatePickerComponent extends AbstractDatePickerDirective {
}
this.datePickerInstance = new DatePicker(
this.injector,
`#${this.id}`,
initialValue,
options,
null,
this.configurationService,
);
}
}

@ -63,8 +63,8 @@ $datepicker--border-radius: 5px
border-radius: $datepicker--border-radius
box-shadow: none !important
&:nth-child(7n+7),
&:nth-child(7n+6)
&.flatpickr-disabled,
&.flatpickr-non-working-day
background: $spot-color-basic-gray-6
border-radius: 0

@ -0,0 +1,91 @@
#-- 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.
#++
require 'spec_helper'
require 'features/page_objects/notification'
require 'features/work_packages/details/inplace_editor/shared_examples'
require 'features/work_packages/shared_contexts'
require 'support/edit_fields/edit_field'
require 'features/work_packages/work_packages_page'
describe 'Work packages datepicker workdays',
with_settings: { date_format: '%Y-%m-%d' },
js: true do
shared_let(:project) { create :project_with_types, public: true }
shared_let(:work_package) { create :work_package, project:, start_date: Date.parse('2022-01-01') }
shared_let(:user) { create :admin }
shared_let(:work_packages_page) { Pages::FullWorkPackage.new(work_package, project) }
let(:combined_date) { work_packages_page.edit_field(:combinedDate) }
before do
login_as(user)
work_packages_page.visit!
work_packages_page.ensure_page_loaded
combined_date.activate!
combined_date.expect_active!
end
context 'with default work days' do
let!(:week_days) { create :week_days }
it 'shows them as disabled' do
expect(page).to have_selector('.dayContainer', count: 2)
weekend_days = %w[1 2 8 9 15 16 22 23 29 30].map(&:to_i)
weekend_days.each do |weekend_day|
expect(page).to have_selector('.dayContainer:first-of-type .flatpickr-day.flatpickr-non-working-day',
text: weekend_day,
exact_text: true)
end
((1..31).to_a - weekend_days).each do |workday|
expect(page).to have_selector('.dayContainer:first-of-type .flatpickr-day:not(.flatpickr-non-working-day)',
text: workday,
exact_text: true)
end
end
end
context 'with all days marked as weekend' do
let!(:week_days) do
days = create(:week_days)
WeekDay.update_all(working: false)
days
end
it 'shows them as disabled' do
expect(page).to have_selector('.dayContainer', count: 2)
expect(page).to have_selector('.dayContainer:first-of-type .flatpickr-day.flatpickr-non-working-day', count: 31)
end
end
end
Loading…
Cancel
Save