diff --git a/lib/content-services/src/lib/content-node-share/content-node-share.dialog.html b/lib/content-services/src/lib/content-node-share/content-node-share.dialog.html index 034b6664d6..8422736942 100644 --- a/lib/content-services/src/lib/content-node-share/content-node-share.dialog.html +++ b/lib/content-services/src/lib/content-node-share/content-node-share.dialog.html @@ -51,7 +51,12 @@ [attr.aria-label]="'SHARE.EXPIRATION-LABEL' | translate" [min]="minDate" formControlName="time" - [matDatepicker]="datePicker" /> + (keydown)="preventIncorrectCharacters($event)" + (blur)="onTimeChanged()" + (keydown.enter)="datePickerInput.blur()" + [matDatepicker]="datePicker"/> + {{ 'SHARE.INVALID_DATE_ERROR' | translate }} diff --git a/lib/content-services/src/lib/content-node-share/content-node-share.dialog.spec.ts b/lib/content-services/src/lib/content-node-share/content-node-share.dialog.spec.ts index 907d659765..a49f68c42e 100644 --- a/lib/content-services/src/lib/content-node-share/content-node-share.dialog.spec.ts +++ b/lib/content-services/src/lib/content-node-share/content-node-share.dialog.spec.ts @@ -21,12 +21,12 @@ import { of } from 'rxjs'; import { NotificationService, AppConfigService } from '@alfresco/adf-core'; import { NodesApiService } from '../common/services/nodes-api.service'; import { RenditionService } from '../common/services/rendition.service'; - import { SharedLinksApiService } from './services/shared-links-api.service'; import { ShareDialogComponent } from './content-node-share.dialog'; import { ContentTestingModule } from '../testing/content.testing.module'; import { TranslateModule } from '@ngx-translate/core'; import { format, endOfDay } from 'date-fns'; +import { By } from '@angular/platform-browser'; describe('ShareDialogComponent', () => { let node; @@ -45,6 +45,17 @@ describe('ShareDialogComponent', () => { const getShareToggleLinkedClasses = (): DOMTokenList => fixture.nativeElement.querySelector(shareToggleId).classList; + const fillInDatepickerInput = (value: string) => { + const input = fixture.debugElement.query(By.css('.adf-share-link__input')).nativeElement; + input.value = value; + input.dispatchEvent(new Event('input')); + input.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + }; + + const clickExpireToggleButton = () => fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label') + .dispatchEvent(new MouseEvent('click')); + const clickShareToggleButton = () => fixture.nativeElement.querySelector(`${shareToggleId} label`) .dispatchEvent(new MouseEvent('click')); @@ -250,17 +261,10 @@ describe('ShareDialogComponent', () => { }; fixture.detectChanges(); - component.form.controls['time'].setValue(new Date()); - fixture.detectChanges(); - - fixture.nativeElement - .querySelector('.mat-slide-toggle[data-automation-id="adf-expire-toggle"] label') - .dispatchEvent(new MouseEvent('click')); - + clickExpireToggleButton(); fixture.detectChanges(); - await fixture.whenStable(); expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalled(); @@ -304,10 +308,10 @@ describe('ShareDialogComponent', () => { spyOn(appConfigService, 'get').and.callFake(() => dateTimePickerType as any); fixture.detectChanges(); - fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label') - .dispatchEvent(new MouseEvent('click')); + clickExpireToggleButton(); fixture.componentInstance.time.setValue(date); + component.onTimeChanged(); fixture.detectChanges(); tick(500); @@ -326,11 +330,11 @@ describe('ShareDialogComponent', () => { spyOn(appConfigService, 'get').and.returnValue(dateTimePickerType); fixture.detectChanges(); - fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label') - .dispatchEvent(new MouseEvent('click')); + clickExpireToggleButton(); fixture.componentInstance.type = 'datetime'; fixture.componentInstance.time.setValue(date); + component.onTimeChanged(); fixture.detectChanges(); tick(100); @@ -342,5 +346,44 @@ describe('ShareDialogComponent', () => { expiresAt: expiryDate }); })); + + it('should not update node when provided date is less than minDate', () => { + fixture.detectChanges(); + clickExpireToggleButton(); + fillInDatepickerInput('01.01.2010'); + + expect(component.form.invalid).toBeTrue(); + expect(sharedLinksApiService.deleteSharedLink).not.toHaveBeenCalled(); + expect(sharedLinksApiService.createSharedLinks).not.toHaveBeenCalled(); + }); + + it('should not accept alphabets in the datepicker input', () => { + fixture.detectChanges(); + clickExpireToggleButton(); + fillInDatepickerInput('test'); + + expect(component.form.invalid).toBeTrue(); + expect(component.form.controls['time'].value).toBeNull(); + }); + + it('should show an error if provided date is invalid', () => { + fixture.detectChanges(); + clickExpireToggleButton(); + fillInDatepickerInput('32'); + const error = fixture.debugElement.query(By.css('[data-automation-id="adf-share-link-input-warning"]')); + + expect(error).toBeTruthy(); + expect(component.time.hasError('invalidDate')).toBeTrue(); + }); + + it('should not show an error when provided date is valid', () => { + fixture.detectChanges(); + clickExpireToggleButton(); + fillInDatepickerInput('12.12.2525'); + const error = fixture.debugElement.query(By.css('[data-automation-id="adf-share-link-input-warning"]')); + + expect(error).toBeNull(); + expect(component.time.hasError('invalidDate')).toBeFalse(); + }); }); }); diff --git a/lib/content-services/src/lib/content-node-share/content-node-share.dialog.ts b/lib/content-services/src/lib/content-node-share/content-node-share.dialog.ts index 5b48aaeb45..6beaaf69d4 100644 --- a/lib/content-services/src/lib/content-node-share/content-node-share.dialog.ts +++ b/lib/content-services/src/lib/content-node-share/content-node-share.dialog.ts @@ -18,17 +18,21 @@ import { Component, Inject, OnInit, ViewEncapsulation, ViewChild, OnDestroy } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; -import { UntypedFormGroup, UntypedFormControl, AbstractControl } from '@angular/forms'; +import { + UntypedFormGroup, + UntypedFormControl, + AbstractControl, + Validators, + ValidationErrors +} from '@angular/forms'; import { Subject } from 'rxjs'; import { ContentService } from '../common/services/content.service'; - import { SharedLinksApiService } from './services/shared-links-api.service'; import { SharedLinkBodyCreate, SharedLinkEntry } from '@alfresco/js-api'; import { ConfirmDialogComponent } from '../dialogs/confirm.dialog'; import { ContentNodeShareSettings } from './content-node-share.settings'; -import { takeUntil, debounceTime } from 'rxjs/operators'; import { RenditionService } from '../common/services/rendition.service'; -import { format, add, endOfDay } from 'date-fns'; +import { format, add, endOfDay, isBefore } from 'date-fns'; type DatePickerType = 'date' | 'time' | 'month' | 'datetime'; @@ -40,6 +44,10 @@ type DatePickerType = 'date' | 'time' | 'month' | 'datetime'; encapsulation: ViewEncapsulation.None }) export class ShareDialogComponent implements OnInit, OnDestroy { + private minDateValidator = (control: AbstractControl): ValidationErrors | null => { + return isBefore(endOfDay(new Date(control.value)), this.minDate) ? {invalidDate: true} : null; + }; + minDate = add(new Date(), { days: 1 }); sharedId: string; fileName: string; @@ -49,7 +57,7 @@ export class ShareDialogComponent implements OnInit, OnDestroy { isLinkWithExpiryDate = false; form: UntypedFormGroup = new UntypedFormGroup({ sharedUrl: new UntypedFormControl(''), - time: new UntypedFormControl({ value: '', disabled: true }) + time: new UntypedFormControl({value: '', disabled: true}, [Validators.required, this.minDateValidator]) }); type: DatePickerType = 'date'; maxDebounceTime = 500; @@ -90,12 +98,12 @@ export class ShareDialogComponent implements OnInit, OnDestroy { this.isLinkWithExpiryDate ? this.time.enable() : this.time.disable(); } } - - this.time.valueChanges.pipe(debounceTime(this.maxDebounceTime), takeUntil(this.onDestroy$)).subscribe((value) => this.onTimeChanged(value)); } - onTimeChanged(date: Date) { - this.updateNode(date); + onTimeChanged() { + if (this.time.valid) { + this.updateNode(this.time.value); + } } get time(): AbstractControl { @@ -137,12 +145,17 @@ export class ShareDialogComponent implements OnInit, OnDestroy { } onDatePickerClosed() { - this.datePickerInput.nativeElement.blur(); + this.onTimeChanged(); if (!this.time.value) { this.slideToggleExpirationDate.checked = false; } } + preventIncorrectCharacters(e: KeyboardEvent): boolean { + const regex = /[^\d/.-]/; + return e.key.length === 1 ? !regex.test(e.key) : true; + } + private openConfirmationDialog() { this.isFileShared = false; diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index 022006237f..7b10cb8cf7 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -533,7 +533,8 @@ "REMOVE": "Remove" }, "UNSHARE_ERROR": "Error unsharing this file", - "UNSHARE_PERMISSION_ERROR": "You don't have permission to unshare this file" + "UNSHARE_PERMISSION_ERROR": "You don't have permission to unshare this file", + "INVALID_DATE_ERROR": "Invalid date" }, "PERMISSION_MANAGER": { "PERMISSION_DISPLAY": {