Merge remote-tracking branch 'origin/release/12.3' into dev

pull/11400/head
Christophe Bliard 2 years ago
commit 5ed81dbb98
No known key found for this signature in database
GPG Key ID: 2BC07603210C3FA4
  1. 5
      app/seeders/root_seeder.rb
  2. 2
      docker/dev/frontend/Dockerfile
  3. 28
      frontend/src/app/features/calendar/wp-calendar/wp-calendar.component.ts
  4. 39
      frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts
  5. 2
      frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.spec.ts
  6. 11
      frontend/src/app/shared/components/datepicker/multi-date-modal/multi-date.modal.html
  7. 15
      frontend/src/app/shared/components/datepicker/multi-date-modal/multi-date.modal.ts
  8. 8
      frontend/src/app/shared/components/datepicker/single-date-modal/single-date.modal.html
  9. 14
      frontend/src/app/shared/components/datepicker/single-date-modal/single-date.modal.ts
  10. 9
      frontend/src/app/shared/components/datepicker/styles/datepicker_mobile.modal.sass
  11. 10
      frontend/src/app/shared/components/fields/edit/field-types/date-picker-edit-field.component.ts
  12. 2
      frontend/src/app/spot/components/text-field/text-field.component.html
  13. 11
      frontend/src/app/spot/components/text-field/text-field.component.ts
  14. 2
      frontend/src/app/spot/styles/sass/variables/zindex.sass
  15. 1
      frontend/src/global_styles/content/_datepicker.sass
  16. 3
      frontend/src/global_styles/content/_toast.sass
  17. 8
      frontend/src/global_styles/layout/_main_menu_mobile.sass

@ -88,7 +88,10 @@ class RootSeeder < Seeder
##
# Clears some schema caches and column information.
def reset_active_record!
ActiveRecord::Base.descendants.each do |klass|
ActiveRecord::Base
.descendants
.reject(&:abstract_class?)
.each do |klass|
klass.connection.schema_cache.clear!
klass.reset_column_information
end

@ -8,6 +8,8 @@ ENV USER=node
RUN apt-get update && apt-get install -y chromium
ENV CHROME_BIN=/usr/bin/chromium
RUN npm i -g npm
RUN groupadd $USER || true

@ -99,6 +99,10 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
private alreadyLoaded = false;
text = {
cannot_drag_to_non_working_day: this.I18n.t('js.team_planner.cannot_drag_to_non_working_day'),
};
constructor(
readonly states:States,
readonly $state:StateService,
@ -110,6 +114,7 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
readonly i18n:I18nService,
readonly toastService:ToastService,
private sanitizer:DomSanitizer,
private I18n:I18nService,
private configuration:ConfigurationService,
readonly calendar:OpCalendarService,
readonly workPackagesCalendar:OpWorkPackagesCalendarService,
@ -202,8 +207,27 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
const workPackage = event.extendedProps.workPackage as WorkPackageResource;
el.dataset.workPackageId = workPackage.id as string;
},
eventResize: (resizeInfo:EventResizeDoneArg) => this.updateEvent(resizeInfo),
eventDrop: (dropInfo:EventDropArg) => this.updateEvent(dropInfo),
eventResize: (resizeInfo:EventResizeDoneArg) => {
const due = moment(resizeInfo.event.endStr).subtract(1, 'day').toDate();
const start = moment(resizeInfo.event.startStr).toDate();
const wp = resizeInfo.event.extendedProps.workPackage as WorkPackageResource;
if (!wp.ignoreNonWorkingDays && (this.weekdayService.isNonWorkingDay(start) || this.weekdayService.isNonWorkingDay(due))) {
this.toastService.addError(this.text.cannot_drag_to_non_working_day);
resizeInfo?.revert();
return;
}
void this.updateEvent(resizeInfo);
},
eventDrop: (dropInfo:EventDropArg) => {
const start = moment(dropInfo.event.startStr).toDate();
const wp = dropInfo.event.extendedProps.workPackage as WorkPackageResource;
if (!wp.ignoreNonWorkingDays && this.weekdayService.isNonWorkingDay(start)) {
this.toastService.addError(this.text.cannot_drag_to_non_working_day);
dropInfo?.revert();
return;
}
void this.updateEvent(dropInfo);
},
eventResizeStart: (resizeInfo:EventResizeDoneArg) => {
const wp = resizeInfo.event.extendedProps.workPackage as WorkPackageResource;
if (!wp.ignoreNonWorkingDays) {

@ -497,7 +497,17 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
// DnD configuration
editable: true,
droppable: true,
eventResize: (resizeInfo:EventResizeDoneArg) => this.updateEvent(resizeInfo),
eventResize: (resizeInfo:EventResizeDoneArg) => {
const due = moment(resizeInfo.event.endStr).subtract(1, 'day').toDate();
const start = moment(resizeInfo.event.startStr).toDate();
const wp = resizeInfo.event.extendedProps.workPackage as WorkPackageResource;
if (!wp.ignoreNonWorkingDays && (this.weekdayService.isNonWorkingDay(start) || this.weekdayService.isNonWorkingDay(due))) {
this.toastService.addError(this.text.cannot_drag_to_non_working_day);
resizeInfo?.revert();
return;
}
void this.updateEvent(resizeInfo);
},
eventResizeStart: (resizeInfo:EventResizeDoneArg) => {
const wp = resizeInfo.event.extendedProps.workPackage as WorkPackageResource;
if (!wp.ignoreNonWorkingDays) {
@ -521,10 +531,26 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
this.draggingItem$.next(undefined);
this.removeBackGroundEvents();
},
eventDrop: (dropInfo:EventDropArg) => this.updateEvent(dropInfo),
eventDrop: (dropInfo:EventDropArg) => {
const start = moment(dropInfo.event.startStr).toDate();
const wp = dropInfo.event.extendedProps.workPackage as WorkPackageResource;
if (!wp.ignoreNonWorkingDays && this.weekdayService.isNonWorkingDay(start)) {
this.toastService.addError(this.text.cannot_drag_to_non_working_day);
dropInfo?.revert();
return;
}
void this.updateEvent(dropInfo);
},
eventReceive: async (dropInfo:EventReceiveArg) => {
await this.updateEvent(dropInfo);
const due = moment(dropInfo.event.endStr).subtract(1, 'day').toDate();
const start = moment(dropInfo.event.startStr).toDate();
const wp = dropInfo.event.extendedProps.workPackage as WorkPackageResource;
if (!wp.ignoreNonWorkingDays && (this.weekdayService.isNonWorkingDay(start) || this.weekdayService.isNonWorkingDay(due))) {
this.toastService.addError(this.text.cannot_drag_to_non_working_day);
dropInfo?.revert();
return;
}
await this.updateEvent(dropInfo);
this.actions$.dispatch(teamPlannerEventAdded({ workPackage: wp.id as string }));
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@ -813,13 +839,6 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
private async updateEvent(info:EventResizeDoneArg|EventDropArg|EventReceiveArg):Promise<void> {
const changeset = this.workPackagesCalendar.updateDates(info);
const due = moment(info.event.endStr).subtract(1, 'day').toDate();
const start = moment(info.event.startStr).toDate();
if (!changeset.projectedResource.ignoreNonWorkingDays && (this.weekdayService.isNonWorkingDay(start) || this.weekdayService.isNonWorkingDay(due))) {
this.toastService.addError(this.text.cannot_drag_to_non_working_day);
info?.revert();
return;
}
const resource = info.event.getResources()[0];
if (resource) {
changeset.setValue('assignee', { href: resource.id });

@ -43,7 +43,7 @@ describe('WpTabsService', () => {
});
service = TestBed.inject(WorkPackageTabsService);
(service as any).registeredTabs = [];
service.register(displayableTab, notDisplayableTab);
service.register({ ...displayableTab }, { ...notDisplayableTab });
});
describe('displayableTabs()', () => {

@ -4,6 +4,10 @@
[attr.id]="htmlId"
#modalContainer
data-indicator-name="modal"
tabindex="0"
cdkFocusInitial
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
(submit)="save($event)"
>
<op-datepicker-banner [scheduleManually]="scheduleManually"></op-datepicker-banner>
@ -39,6 +43,8 @@
(ngModelChange)="startDateChanged$.next($event)"
[disabled]="!isSchedulable"
[showClearButton]="currentlyActivatedDateField === 'start'"
[pattern]="datePattern"
inputmode="numeric"
(focusin)="setCurrentActivatedField('start')"
></spot-text-field>
<button
@ -65,6 +71,8 @@
(ngModelChange)="endDateChanged$.next($event)"
[disabled]="!isSchedulable"
[showClearButton]="currentlyActivatedDateField === 'end'"
[pattern]="datePattern"
inputmode="numeric"
(focusin)="setCurrentActivatedField('end')"
></spot-text-field>
<button
@ -90,6 +98,8 @@
[ngClass]="{'op-datepicker-modal--date-field_current' : showFieldAsActive('duration')}"
[ngModel]="durationFocused ? duration : displayedDuration"
[showClearButton]="durationFocused"
pattern="\d*"
inputmode="numeric"
(ngModelChange)="durationChanges$.next($event)"
[disabled]="!isSchedulable"
(focusin)="handleDurationFocusIn()"
@ -105,6 +115,7 @@
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
(click)="cancel()"
class="op-datepicker-modal--action button button_no-margin spot-action-bar--action"
data-qa-selector="op-datepicker-modal--action"

@ -46,7 +46,6 @@ import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.servi
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 { 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';
@ -77,8 +76,9 @@ import {
validDate,
} from 'core-app/shared/components/datepicker/helpers/date-modal.helpers';
import { WeekdayService } from 'core-app/core/days/weekday.service';
import DateOption = flatpickr.Options.DateOption;
import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper';
import { DeviceService } from 'core-app/core/browser/device.service';
import DateOption = flatpickr.Options.DateOption;
export type DateKeys = 'start'|'end';
export type DateFields = DateKeys|'duration';
@ -118,7 +118,7 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
@InjectField() dateModalRelations:DateModalRelationsService;
@InjectField() browserDetector:BrowserDetector;
@InjectField() deviceService:DeviceService;
@InjectField() weekdayService:WeekdayService;
@ -151,6 +151,8 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
htmlId = '';
datePattern = '[0-9]{4}-[0-9]{2}-[0-9]{2}';
dates:{ [key in DateKeys]:string|null } = {
start: null,
end: null,
@ -369,6 +371,10 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
// 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',
@ -457,9 +463,10 @@ export class MultiDateModalComponent extends OpModalComponent implements AfterVi
[this.dates.start || '', this.dates.end || ''],
{
mode: 'range',
showMonths: this.browserDetector.isMobile ? 1 : 2,
showMonths: this.deviceService.isMobile ? 1 : 2,
inline: true,
onReady: (_date, _datestr, instance) => {
instance.calendarContainer.classList.add('op-datepicker-modal--flatpickr-instance');
this.reposition(jQuery(this.modalContainer.nativeElement), jQuery(`.${activeFieldContainerClassName}`));
this.ensureHoveredSelection(instance.calendarContainer);
},

@ -5,6 +5,10 @@
#modalContainer
data-indicator-name="modal"
(submit)="save($event)"
tabindex="0"
cdkFocusInitial
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<op-datepicker-banner [scheduleManually]="scheduleManually"></op-datepicker-banner>
@ -34,6 +38,8 @@
[(ngModel)]="date"
(ngModelChange)="dateChangedManually$.next()"
[showClearButton]="true"
[pattern]="datePattern"
inputmode="numeric"
></spot-text-field>
<button
slot="action"
@ -53,12 +59,14 @@
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
(click)="cancel()"
class="op-datepicker-modal--action 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"

@ -71,6 +71,7 @@ import {
setDates,
validDate,
} from 'core-app/shared/components/datepicker/helpers/date-modal.helpers';
import { DeviceService } from 'core-app/core/browser/device.service';
@Component({
templateUrl: './single-date.modal.html',
@ -93,7 +94,7 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
@InjectField() dateModalRelations:DateModalRelationsService;
@InjectField() browserDetector:BrowserDetector;
@InjectField() deviceService:DeviceService;
@ViewChild('modalContainer') modalContainer:ElementRef<HTMLElement>;
@ -113,6 +114,8 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
htmlId = '';
datePattern = '[0-9]{4}-[0-9]{2}-[0-9]{2}';
date:string|null = null;
dateChangedManually$ = new Subject<void>();
@ -245,6 +248,10 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
// 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',
@ -261,9 +268,10 @@ export class SingleDateModalComponent extends OpModalComponent implements AfterV
this.date || '',
{
mode: 'single',
showMonths: this.browserDetector.isMobile ? 1 : 2,
showMonths: this.deviceService.isMobile ? 1 : 2,
inline: true,
onReady: () => {
onReady: (_date, _datestr, instance) => {
instance.calendarContainer.classList.add('op-datepicker-modal--flatpickr-instance');
this.reposition(jQuery(this.modalContainer.nativeElement), jQuery(`.${activeFieldContainerClassName}`));
},
onChange: (dates:Date[]) => {

@ -2,8 +2,13 @@
@media screen and (max-width: 679px)
.op-datepicker-modal
height: initial
max-width: 300px
// Use same width as spot-modal mobile
width: calc(100vw - 2rem)
margin-bottom: 0
&--flatpickr-instance
align-self: center
&--dates-container
grid-template-columns: 1fr 1fr

@ -39,6 +39,7 @@ import { EditFieldComponent } from 'core-app/shared/components/fields/edit/edit-
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 {
@ -46,6 +47,8 @@ export abstract class DatePickerEditFieldComponent extends EditFieldComponent im
@InjectField() opModalService:OpModalService;
@InjectField() deviceService:DeviceService;
protected modal:SingleDateModalComponent|MultiDateModalComponent;
ngOnInit():void {
@ -70,7 +73,12 @@ export abstract class DatePickerEditFieldComponent extends EditFieldComponent im
const component = this.change.schema.isMilestone ? SingleDateModalComponent : MultiDateModalComponent;
this.modal = this
.opModalService
.show<SingleDateModalComponent|MultiDateModalComponent>(component, this.injector, { changeset: this.change, fieldName: this.name }, true);
.show<SingleDateModalComponent|MultiDateModalComponent>(
component,
this.injector,
{ changeset: this.change, fieldName: this.name },
!this.deviceService.isMobile,
);
const { modal } = this;

@ -2,6 +2,8 @@
<input
class="spot-text-field--input"
[attr.name]="name"
[attr.pattern]="pattern"
[attr.inputmode]="inputmode"
[disabled]="disabled"
[placeholder]="placeholder"
[ngModel]="value"

@ -67,6 +67,17 @@ export class SpotTextFieldComponent implements ControlValueAccessor {
*/
@Input() public value = '';
/**
* The html input (Regexp) pattern to provide hints to keyboards what layout to use
* and to aid in validation.
*/
@Input() public pattern:string|undefined;
/**
* The html inputmode to hint virtual keyboard layouts.
*/
@Input() public inputmode:'text'|'decimal'|'numeric'|'tel'|'search'|'email'|'url' = 'text';
valueChanged(value:string):void {
this.writeValue(value);
this.onChange(value);

@ -1,6 +1,6 @@
@use "sass:map"
$spot-z-indexes: ("toast": 300, "tooltip": 500, "header": 800, "drop-modal": 1000)
$spot-z-indexes: ("toast": 300, "tooltip": 500, "main-menu": 600, "header": 800, "drop-modal": 1000)
@mixin spot-z-index($type, $addition: 0)
z-index: map.get($spot-z-indexes, $type) + $addition

@ -67,6 +67,7 @@ $datepicker--border-radius: 5px
.flatpickr-current-month
@include spot-body-small(bold)
justify-content: center
align-items: center
.numInputWrapper
position: relative

@ -295,6 +295,8 @@ $nm-upload-box-padding: rem-calc(15) rem-calc(25)
// Use same styles for flash messages
.flash, #errorExplanation
// Always above the toast
@include spot-z-index("toast", 1)
border-radius: $nm-border-radius
border-style: solid
border-width: rem-calc(1)
@ -304,7 +306,6 @@ $nm-upload-box-padding: rem-calc(15) rem-calc(25)
box-shadow: rem-calc(1px 2px 3px) rgba(0, 0, 0, 0.2)
margin-bottom: 0.1875rem
margin-top: 0.5rem
z-index: 15
&.ng-leave
@include animation(0.5s fade-out)

@ -28,14 +28,14 @@
@include breakpoint(680px down)
.main-menu
@include spot-z-index("main-menu")
position: fixed
z-index: 12
border-bottom: 1px solid var(--main-menu-border-color)
border: none
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15)
min-width: 75vw
#main-menu-toggle
#main-menu-toggle
.icon-hamburger
display: none
.icon-close
@ -44,7 +44,7 @@
.hidden-navigation
.main-menu
display: none
#main-menu-toggle
#main-menu-toggle
.icon-hamburger
display: block
.icon-close
@ -76,4 +76,4 @@
bottom: 0
right: 0
opacity: .4
z-index: 11
@include spot-z-index("main-menu", -1)

Loading…
Cancel
Save