[PRODSEC-6575] Shared link not accessible now after the expiry date which was earlier accessible even after that expiration date (#8540)

* Shared link expiry date code implementation

* test case updated

* removed unsed code

* design changes as per the new design of Share dialog

* test cases modification as per new design changes

* placeholder modified for expiration date

* look and feel changes for share dialog as per Shane comments

* boolean name changed as per naming convention

* review comments addressed

* review comments addressed

* type specified for node object

* linting corrections

* resolved nested ternary date operation into an independent statement

* review comments addressed

* used date-fns instead of moment.js in code as well as in test cases

* review comments for date-fns addressed

* removed extra line

* removed extra empty lines in template

* import changes and indentation correction

* error in console resolved which was occuring after selecting date and time

* used mat-datepicker instead of mat-datetimepicker

* package-lock.json file updated for date-fns implementation

* made type  'date' as default and removed the settings coming from the ACA

* unit test case modifications as per calender changes

* e2e modifications as per new calendar component
This commit is contained in:
Jatin Chugh 2023-06-14 15:30:08 +05:30 committed by GitHub
parent 95927c6319
commit 479c96eabb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 361 additions and 225 deletions

View File

@ -132,23 +132,6 @@ describe('Share file', () => {
await BrowserActions.closeMenuAndDialogs(); await BrowserActions.closeMenuAndDialogs();
}); });
it('[C286548] Should be possible to set expiry date for link', async () => {
await contentServicesPage.clickShareButton();
await shareDialog.checkDialogIsDisplayed();
await shareDialog.clickExpireToggle();
await shareDialog.setDefaultDay();
await shareDialog.setDefaultHour();
await shareDialog.setDefaultMinutes();
await shareDialog.dateTimePickerDialogIsClosed();
const value = await shareDialog.getExpirationDate();
await shareDialog.clickCloseButton();
await shareDialog.dialogIsClosed();
await contentServicesPage.clickShareButton();
await shareDialog.checkDialogIsDisplayed();
await shareDialog.expirationDateInputHasValue(value);
await BrowserActions.closeMenuAndDialogs();
});
it('[C310329] Should be possible to set expiry date only for link', async () => { it('[C310329] Should be possible to set expiry date only for link', async () => {
await LocalStorageUtil.setConfigField('sharedLinkDateTimePickerType', JSON.stringify('date')); await LocalStorageUtil.setConfigField('sharedLinkDateTimePickerType', JSON.stringify('date'));
await contentServicesPage.clickShareButton(); await contentServicesPage.clickShareButton();

View File

@ -17,8 +17,7 @@
import { $$, $ } from 'protractor'; import { $$, $ } from 'protractor';
import { BrowserVisibility, TogglePage, BrowserActions, DateTimePickerPage } from '@alfresco/adf-testing'; import { BrowserVisibility, TogglePage, BrowserActions, DateTimePickerPage } from '@alfresco/adf-testing';
import * as moment from 'moment'; import { format, add } from 'date-fns';
export class ShareDialogPage { export class ShareDialogPage {
togglePage = new TogglePage(); togglePage = new TogglePage();
@ -85,7 +84,7 @@ export class ShareDialogPage {
} }
async calendarTodayDayIsDisabled(): Promise<void> { async calendarTodayDayIsDisabled(): Promise<void> {
const tomorrow = moment().add(1, 'days').format('D'); const tomorrow = format(add(new Date(), {days: 1}), 'd');
if (tomorrow !== '1') { if (tomorrow !== '1') {
await this.dateTimePickerPage.checkCalendarTodayDayIsDisabled(); await this.dateTimePickerPage.checkCalendarTodayDayIsDisabled();
@ -93,7 +92,7 @@ export class ShareDialogPage {
} }
async setDefaultDay(): Promise<void> { async setDefaultDay(): Promise<void> {
const tomorrow = moment().add(1, 'days').format('D'); const tomorrow = format(add(new Date(), {days: 1}), 'd');
await this.dateTimePickerPage.setDate(tomorrow); await this.dateTimePickerPage.setDate(tomorrow);
} }

View File

@ -1,14 +1,61 @@
<div class="adf-share-link__dialog-content"> <div class="adf-share-link__dialog-content">
<div data-automation-id="adf-share-dialog-title" class="adf-share-link__title" role="heading" aria-level="1"> <div class="adf-share-link__dialog-container">
<div class="adf-share-link--row">
<div
data-automation-id="adf-share-dialog-title"
class="adf-share-link__title adf-share-link__label adf-share-link__heading"
role="heading"
aria-level="1">
{{ 'SHARE.DIALOG-TITLE' | translate }} {{ fileName }} {{ 'SHARE.DIALOG-TITLE' | translate }} {{ fileName }}
</div> </div>
<mat-icon mat-dialog-close class="adf-share-link__close adf-share-link__icon">close</mat-icon>
</div>
<mat-dialog-content> <mat-dialog-content>
<p class="adf-share-link__info">{{ 'SHARE.DESCRIPTION' | translate }}</p> <hr class="adf-share-link__separation-line" />
<form [formGroup]="form" class="adf-share-link__form">
<div class="adf-share-link--row">
<mat-icon class="adf-share-link__icon">timer</mat-icon>
<div class="adf-share-link__label adf-sharable-link">{{ 'SHARE.LINK-EXPIRY-DATE' | translate }}
</div>
<mat-slide-toggle
#slideToggleExpirationDate
[disabled]="!canUpdate"
color="primary"
data-automation-id="adf-expire-toggle"
aria-label="{{ 'SHARE.EXPIRES' | translate }}"
[checked]="time.value"
(change)="onToggleExpirationDate($event)">
</mat-slide-toggle>
</div>
<div
[style.display]="isExpiryDateToggleChecked ? 'block' : 'none'"
data-automation-id="adf-slide-toggle-checked"
class="adf-share-link__date-time-container">
<mat-form-field class="adf-full-width adf-float-label" floatLabel='always'>
<mat-label>{{ 'SHARE.EXPIRATION-PLACEHOLDER' | translate }}</mat-label>
<mat-datepicker-toggle
[disabled]="time.disabled"
[for]="datePicker"
matSuffix
class="adf-share-link__icon adf-share-link__calender-icon">
</mat-datepicker-toggle>
<mat-datepicker
#datePicker
(closed)="onDatePickerClosed()">
</mat-datepicker>
<input
class="adf-share-link__input"
#datePickerInput
matInput
placeholder="{{ 'SHARE.EXPIRATION-PLACEHOLDER' | translate }}"
[attr.aria-label]="'SHARE.EXPIRATION-LABEL' | translate"
[min]="minDate"
formControlName="time"
[matDatepicker]="datePicker" />
</mat-form-field>
</div>
<p class="adf-share-link__info adf-share-link__para">{{ 'SHARE.SHARE-LINK' | translate }}</p>
<div class="adf-share-link--row"> <div class="adf-share-link--row">
<div class="adf-share-link__label">{{ 'SHARE.TITLE' | translate }}</div>
<mat-slide-toggle <mat-slide-toggle
color="primary" color="primary"
data-automation-id="adf-share-toggle" data-automation-id="adf-share-toggle"
@ -17,10 +64,13 @@
[disabled]="!canUpdate || isDisabled" [disabled]="!canUpdate || isDisabled"
(change)="onSlideShareChange($event)"> (change)="onSlideShareChange($event)">
</mat-slide-toggle> </mat-slide-toggle>
<div class="adf-share-link__label adf-sharable-link">{{ 'SHARE.SHARABLE-LINK-CREATED' | translate }}
</div> </div>
</div>
<form [formGroup]="form"> <mat-form-field
<mat-form-field class="adf-full-width adf-float-label" floatLabel='always'> class="adf-full-width adf-float-label"
floatLabel='always'
[ngClass]="isLinkWithExpiryDate? 'adf-share-link__border-color' : ''">
<input <input
#sharedLinkInput #sharedLinkInput
data-automation-id="adf-share-link" data-automation-id="adf-share-link"
@ -32,58 +82,34 @@
formControlName="sharedUrl" formControlName="sharedUrl"
readonly="readonly"> readonly="readonly">
<mat-icon <mat-icon
class="adf-input-action" class="adf-input-action adf-share-link__icon"
role="button" role="button"
matSuffix matSuffix
[clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate" [adf-clipboard] [clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate"
[adf-clipboard]
[attr.aria-label]="'SHARE.COPY_BUTTON_LABEL' | translate" [attr.aria-label]="'SHARE.COPY_BUTTON_LABEL' | translate"
[target]="sharedLinkInput" [target]="sharedLinkInput"
tabindex="0"> tabindex="0">
content_copy content_copy
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<p class="adf-share-link__warn adf-share-link__para" *ngIf="isLinkWithExpiryDate">
{{ 'SHARE.LINK-WITH-EXPIRY-SETTINGS' | translate }}
</p>
<div class="adf-share-link--row"> <div class="adf-share-link--row">
<div class="adf-share-link__label">{{ 'SHARE.EXPIRES' | translate }}</div> <mat-icon class="adf-share-link__icon">public</mat-icon>
<mat-slide-toggle <p
#slideToggleExpirationDate class="adf-share-link__info adf-sharable-link adf-share-link__public-content adf-share-link__para">
[disabled]="!canUpdate" {{ 'SHARE.PUBLIC-CONTENT' | translate }}
color="primary" </p>
data-automation-id="adf-expire-toggle"
aria-label="{{ 'SHARE.EXPIRES' | translate }}"
[checked]="time.value"
(change)="onToggleExpirationDate($event)">
</mat-slide-toggle>
</div> </div>
<mat-form-field class="adf-full-width adf-float-label" floatLabel='always'>
<mat-datetimepicker-toggle
#matDatetimepickerToggle="matDatetimepickerToggle"
[disabled]="time.disabled"
[for]="datetimePicker"
matSuffix>
</mat-datetimepicker-toggle>
<mat-datetimepicker
#datetimePicker
(closed)="onDatetimepickerClosed()"
[type]="type"
[timeInterval]="1">
</mat-datetimepicker>
<input class="adf-share-link__input"
#dateTimePickerInput
matInput
placeholder="{{ 'SHARE.EXPIRATION-LABEL' | translate }}"
[attr.aria-label]="'SHARE.EXPIRATION-LABEL' | translate"
[min]="minDate"
formControlName="time"
[matDatetimepicker]="datetimePicker" />
</mat-form-field>
</form> </form>
<hr class="adf-share-link__separation-line" />
</mat-dialog-content> </mat-dialog-content>
<div mat-dialog-actions> <div mat-dialog-actions>
<button data-automation-id="adf-share-dialog-close" mat-button color="primary" mat-dialog-close> <button data-automation-id="adf-share-dialog-close" mat-button color="primary" mat-dialog-close>
{{ 'SHARE.CLOSE' | translate }} {{ 'SHARE.CLOSE' | translate }}
</button> </button>
</div> </div>
</div>
</div> </div>

View File

@ -7,6 +7,14 @@
&__dialog-content { &__dialog-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 24px;
background-color: var(--theme-grey-text-background-color);
}
&__dialog-container {
background-color: var(--theme-card-background-color);
border-radius: 16px;
padding: 16px 24px;
} }
&__label, &__label,
@ -17,7 +25,7 @@
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-stretch: normal; font-stretch: normal;
font-size: var(--theme-subheading-2-font-size); font-size: var(--theme-body-1-font-size);
color: var(--adf-theme-foreground-text-color-087); color: var(--adf-theme-foreground-text-color-087);
} }
@ -25,22 +33,68 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
&__info { &__form {
padding-top: 8px;
}
&__public-content {
color: var(--adf-theme-foreground-text-color-054); color: var(--adf-theme-foreground-text-color-054);
font-size: var(--theme-caption-font-size); font-size: var(--theme-caption-font-size);
} }
&__warn {
color: var(--theme-warn-color);
font-size: var(--theme-caption-font-size);
}
&--row { &--row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
/* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */ /* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
margin: 8px 0;
} }
&__input { &__input {
color: var(--adf-theme-foreground-text-color-087); color: var(--adf-theme-foreground-text-color-087);
} }
&__separation-line {
border: 1px solid var(--theme-grey-background-color);
margin: 8px -24px;
}
&__close {
cursor: pointer;
margin-left: 12px;
}
&__icon {
color: var(--adf-theme-foreground-icon-color-054);
}
&__para {
margin-top: 8px;
margin-bottom: 8px;
}
&__heading {
font-weight: 700;
font-size: var(--theme-subheading-2-font-size);
}
&__calender-icon {
font-size: 18px;
}
&__date-time-container {
padding-bottom: 8px;
}
&__border-color {
border: 1px solid var(--theme-warn-color);
}
} }
.adf-input-action { .adf-input-action {
@ -49,6 +103,13 @@
.adf-full-width { .adf-full-width {
width: 100%; width: 100%;
background-color: var(--theme-grey-text-background-color);
border-radius: 6px;
padding-top: 8px;
}
.adf-sharable-link {
margin-left: 8px;
} }
.mat-form-field-infix { .mat-form-field-infix {
@ -57,9 +118,13 @@
.mat-dialog-actions { .mat-dialog-actions {
justify-content: flex-end; justify-content: flex-end;
padding: 0;
margin: 8px 0 0;
& > button { & > button {
text-transform: uppercase; color: var(--adf-theme-foreground-base-color);
background-color: var(--theme-grey-text-background-color);
margin-right: 12px;
} }
} }
@ -67,6 +132,20 @@
align-items: center; align-items: center;
} }
.mat-dialog-container {
padding: 0;
}
.mat-form-field-appearance-legacy .mat-form-field-underline {
display: none;
}
.mat-form-field-appearance-legacy .mat-form-field-wrapper {
padding-bottom: 8px;
margin-right: 8px;
margin-left: 8px;
}
@media screen and (max-width: 380px) { @media screen and (max-width: 380px) {
.mat-dialog-container { .mat-dialog-container {
padding: 0 15px; padding: 0 15px;

View File

@ -28,9 +28,9 @@ import { RenditionService } from '../common/services/rendition.service';
import { SharedLinksApiService } from './services/shared-links-api.service'; import { SharedLinksApiService } from './services/shared-links-api.service';
import { ShareDialogComponent } from './content-node-share.dialog'; import { ShareDialogComponent } from './content-node-share.dialog';
import moment from 'moment';
import { ContentTestingModule } from '../testing/content.testing.module'; import { ContentTestingModule } from '../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { format, endOfDay } from 'date-fns';
describe('ShareDialogComponent', () => { describe('ShareDialogComponent', () => {
let node; let node;
@ -45,6 +45,13 @@ describe('ShareDialogComponent', () => {
let component: ShareDialogComponent; let component: ShareDialogComponent;
let appConfigService: AppConfigService; let appConfigService: AppConfigService;
const shareToggleId = '[data-automation-id="adf-share-toggle"]';
const getShareToggleLinkedClasses = (): DOMTokenList => fixture.nativeElement.querySelector(shareToggleId).classList;
const clickShareToggleButton = () => fixture.nativeElement.querySelector(`${shareToggleId} label`)
.dispatchEvent(new MouseEvent('click'));
setupTestBed({ setupTestBed({
imports: [ imports: [
TranslateModule.forRoot(), TranslateModule.forRoot(),
@ -137,7 +144,7 @@ describe('ShareDialogComponent', () => {
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalled(); expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalled();
expect(renditionService.getNodeRendition).toHaveBeenCalled(); expect(renditionService.getNodeRendition).toHaveBeenCalled();
expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId'); expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId');
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked'); expect(getShareToggleLinkedClasses()).toContain('mat-checked');
}); });
it(`should not toggle share action when file has 'sharedId' property`, async () => { it(`should not toggle share action when file has 'sharedId' property`, async () => {
@ -160,7 +167,7 @@ describe('ShareDialogComponent', () => {
expect(sharedLinksApiService.createSharedLinks).not.toHaveBeenCalled(); expect(sharedLinksApiService.createSharedLinks).not.toHaveBeenCalled();
expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId'); expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId');
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked'); expect(getShareToggleLinkedClasses()).toContain('mat-checked');
}); });
it('should open a confirmation dialog when unshare button is triggered', () => { it('should open a confirmation dialog when unshare button is triggered', () => {
@ -176,8 +183,7 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
fixture.nativeElement.querySelector('.mat-slide-toggle label') clickShareToggleButton();
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges(); fixture.detectChanges();
@ -196,8 +202,7 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
fixture.nativeElement.querySelector('.mat-slide-toggle label') clickShareToggleButton();
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges(); fixture.detectChanges();
@ -216,8 +221,7 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
fixture.nativeElement.querySelector('.mat-slide-toggle label') clickShareToggleButton();
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges(); fixture.detectChanges();
@ -235,12 +239,13 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-disabled'); expect(getShareToggleLinkedClasses()).toContain('mat-disabled');
expect(fixture.nativeElement.querySelector('input[formcontrolname="time"]').disabled).toBe(true);
expect(fixture.nativeElement.querySelector('mat-datetimepicker-toggle button').disabled).toBe(true);
}); });
it('should reset expiration date when toggle is unchecked', async () => { it('should delete the current link generated with expiry date and generate a new link without expiry date when toggle is unchecked', async () => {
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of());
spyOn(sharedLinksApiService, 'deleteSharedLink').and.returnValue(of({}));
node.entry.properties['qshare:sharedId'] = 'sharedId'; node.entry.properties['qshare:sharedId'] = 'sharedId';
node.entry.properties['qshare:sharedId'] = '2017-04-15T18:31:37+00:00'; node.entry.properties['qshare:sharedId'] = '2017-04-15T18:31:37+00:00';
node.entry.allowableOperations = ['update']; node.entry.allowableOperations = ['update'];
@ -251,7 +256,7 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
component.form.controls['time'].setValue(moment()); component.form.controls['time'].setValue(new Date());
fixture.detectChanges(); fixture.detectChanges();
@ -263,13 +268,8 @@ describe('ShareDialogComponent', () => {
await fixture.whenStable(); await fixture.whenStable();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalled();
properties: {'qshare:expiryDate': null} expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalledWith('nodeId', undefined);
});
expect(
fixture.nativeElement.querySelector('input[formcontrolname="time"]').value
).toBe('');
}); });
it('should not allow expiration date action when node has no update permission', async () => { it('should not allow expiration date action when node has no update permission', async () => {
@ -284,43 +284,18 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(fixture.nativeElement.querySelector('input[formcontrolname="time"]').disabled).toBe(true);
expect(fixture.nativeElement.querySelector('.mat-slide-toggle[data-automation-id="adf-expire-toggle"]') expect(fixture.nativeElement.querySelector('.mat-slide-toggle[data-automation-id="adf-expire-toggle"]')
.classList).toContain('mat-disabled'); .classList).toContain('mat-disabled');
expect(fixture.nativeElement.querySelector('[data-automation-id="adf-slide-toggle-checked"]').style.display).toEqual('none');
}); });
it('should update node expiration date with selected date', fakeAsync(() => {
const date = moment();
node.entry.allowableOperations = ['update'];
node.entry.properties['qshare:sharedId'] = 'sharedId';
fixture.componentInstance.form.controls['time'].setValue(null);
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
fixture.nativeElement
.querySelector(
'mat-slide-toggle[data-automation-id="adf-expire-toggle"] label'
)
.dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.form.controls['time'].setValue(date);
fixture.detectChanges();
tick(100);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: {'qshare:expiryDate': date.utc().format()}
});
}));
describe('datetimepicker type', () => { describe('datetimepicker type', () => {
beforeEach(() => { beforeEach(() => {
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of(null)); spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of());
spyOn(sharedLinksApiService, 'deleteSharedLink').and.returnValue(of({}));
node.entry.properties['qshare:sharedId'] = 'sharedId';
node.entry.allowableOperations = ['update']; node.entry.allowableOperations = ['update'];
component.data = { component.data = {
node, node,
@ -328,9 +303,9 @@ describe('ShareDialogComponent', () => {
}; };
}); });
it('it should update node with input date and end of day time when type is `date`', fakeAsync(() => { it('should update node with input date and end of day time when type is `date`', fakeAsync(() => {
const dateTimePickerType = 'date'; const dateTimePickerType = 'date';
const date = moment('2525-01-01 13:00:00'); const date = new Date('2525-01-01');
spyOn(appConfigService, 'get').and.callFake(() => dateTimePickerType as any); spyOn(appConfigService, 'get').and.callFake(() => dateTimePickerType as any);
fixture.detectChanges(); fixture.detectChanges();
@ -341,26 +316,35 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
tick(500); tick(500);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { const expiryDate = format(endOfDay(date as Date), `yyyy-MM-dd'T'HH:mm:ss.SSSxx`);
properties: {'qshare:expiryDate': date.endOf('day').utc().format()}
expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalled();
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalledWith('nodeId', {
nodeId: 'nodeId',
expiresAt: expiryDate
}); });
})); }));
it('it should update node with input date and time when type is `datetime`', fakeAsync(() => { it('should update node with input date and time when type is `datetime`', fakeAsync(() => {
const dateTimePickerType = 'datetime'; const dateTimePickerType = 'datetime';
const date = moment('2525-01-01 13:00:00'); const date = new Date('2525-01-01 13:00:00');
spyOn(appConfigService, 'get').and.returnValue(dateTimePickerType); spyOn(appConfigService, 'get').and.returnValue(dateTimePickerType);
fixture.detectChanges(); fixture.detectChanges();
fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label') fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label')
.dispatchEvent(new MouseEvent('click')); .dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.type = 'datetime';
fixture.componentInstance.time.setValue(date); fixture.componentInstance.time.setValue(date);
fixture.detectChanges(); fixture.detectChanges();
tick(100); tick(100);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { const expiryDate = format((new Date(date)), `yyyy-MM-dd'T'HH:mm:ss.SSSxx`);
properties: {'qshare:expiryDate': date.utc().format()}
expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalled();
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalledWith('nodeId', {
nodeId: 'nodeId',
expiresAt: expiryDate
}); });
})); }));
}); });

View File

@ -23,23 +23,19 @@ import {
ViewChild, ViewChild,
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { UntypedFormGroup, UntypedFormControl, AbstractControl } from '@angular/forms'; import { UntypedFormGroup, UntypedFormControl, AbstractControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs'; import { Subject } from 'rxjs';
import {
AppConfigService
} from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { ContentService } from '../common/services/content.service'; import { ContentService } from '../common/services/content.service';
import { SharedLinksApiService } from './services/shared-links-api.service'; import { SharedLinksApiService } from './services/shared-links-api.service';
import { SharedLinkEntry, Node } from '@alfresco/js-api'; import { SharedLinkBodyCreate, SharedLinkEntry } from '@alfresco/js-api';
import { ConfirmDialogComponent } from '../dialogs/confirm.dialog'; import { ConfirmDialogComponent } from '../dialogs/confirm.dialog';
import moment from 'moment';
import { ContentNodeShareSettings } from './content-node-share.settings'; import { ContentNodeShareSettings } from './content-node-share.settings';
import { takeUntil, debounceTime } from 'rxjs/operators'; import { takeUntil, debounceTime } from 'rxjs/operators';
import { RenditionService } from '../common/services/rendition.service'; import { RenditionService } from '../common/services/rendition.service';
import { format, add, endOfDay } from 'date-fns';
type DatePickerType = 'date' | 'time' | 'month' | 'datetime'; type DatePickerType = 'date' | 'time' | 'month' | 'datetime';
@ -52,33 +48,33 @@ type DatePickerType = 'date' | 'time' | 'month' | 'datetime';
}) })
export class ShareDialogComponent implements OnInit, OnDestroy { export class ShareDialogComponent implements OnInit, OnDestroy {
minDate = moment().add(1, 'd'); minDate = add(new Date(), { days: 1 });
sharedId: string; sharedId: string;
fileName: string; fileName: string;
baseShareUrl: string; baseShareUrl: string;
isFileShared: boolean = false; isFileShared = false;
isDisabled: boolean = false; isDisabled = false;
isLinkWithExpiryDate = false;
form: UntypedFormGroup = new UntypedFormGroup({ form: UntypedFormGroup = new UntypedFormGroup({
sharedUrl: new UntypedFormControl(''), sharedUrl: new UntypedFormControl(''),
time: new UntypedFormControl({value: '', disabled: true}) time: new UntypedFormControl({value: '', disabled: true})
}); });
type: DatePickerType = 'datetime'; type: DatePickerType = 'date';
maxDebounceTime = 500; maxDebounceTime = 500;
isExpiryDateToggleChecked: boolean;
@ViewChild('slideToggleExpirationDate', {static: true}) @ViewChild('slideToggleExpirationDate', {static: true})
slideToggleExpirationDate; slideToggleExpirationDate;
@ViewChild('dateTimePickerInput', {static: true}) @ViewChild('datePickerInput', {static: true})
dateTimePickerInput; datePickerInput;
private onDestroy$ = new Subject<boolean>(); private onDestroy$ = new Subject<boolean>();
constructor( constructor(
private appConfigService: AppConfigService,
private sharedLinksApiService: SharedLinksApiService, private sharedLinksApiService: SharedLinksApiService,
private dialogRef: MatDialogRef<ShareDialogComponent>, private dialogRef: MatDialogRef<ShareDialogComponent>,
private dialog: MatDialog, private dialog: MatDialog,
private nodesApiService: NodesApiService,
private contentService: ContentService, private contentService: ContentService,
private renditionService: RenditionService, private renditionService: RenditionService,
@Inject(MAT_DIALOG_DATA) public data: ContentNodeShareSettings @Inject(MAT_DIALOG_DATA) public data: ContentNodeShareSettings
@ -86,8 +82,6 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.type = this.appConfigService.get<DatePickerType>('sharedLinkDateTimePickerType', 'datetime');
if (this.data.node && this.data.node.entry) { if (this.data.node && this.data.node.entry) {
this.fileName = this.data.node.entry.name; this.fileName = this.data.node.entry.name;
this.baseShareUrl = this.data.baseShareUrl; this.baseShareUrl = this.data.baseShareUrl;
@ -99,7 +93,10 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
} else { } else {
this.sharedId = properties['qshare:sharedId']; this.sharedId = properties['qshare:sharedId'];
this.isFileShared = true; this.isFileShared = true;
this.updateForm();
const expiryDate = this.updateForm();
this.isExpiryDateToggleChecked = this.isLinkWithExpiryDate = !!expiryDate;
this.isLinkWithExpiryDate ? this.time.enable() : this.time.disable();
} }
} }
@ -111,10 +108,8 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
.subscribe(value => this.onTimeChanged(value)); .subscribe(value => this.onTimeChanged(value));
} }
onTimeChanged(date: moment.Moment) { onTimeChanged(date: Date) {
this.updateNode(date).subscribe( this.updateNode(date);
() => this.updateEntryExpiryDate(date)
);
} }
get time(): AbstractControl { get time(): AbstractControl {
@ -147,15 +142,16 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
onToggleExpirationDate(slideToggle: MatSlideToggleChange) { onToggleExpirationDate(slideToggle: MatSlideToggleChange) {
if (slideToggle.checked) { if (slideToggle.checked) {
this.time.enable(); this.time.enable();
this.isExpiryDateToggleChecked = true;
} else { } else {
this.time.disable(); this.time.disable();
this.time.setValue(null); this.time.setValue(null);
this.deleteSharedLink(this.sharedId, true);
} }
} }
onDatetimepickerClosed() { onDatePickerClosed() {
this.dateTimePickerInput.nativeElement.blur(); this.datePickerInput.nativeElement.blur();
if (!this.time.value) { if (!this.time.value) {
this.slideToggleExpirationDate.checked = false; this.slideToggleExpirationDate.checked = false;
} }
@ -185,10 +181,10 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}); });
} }
private createSharedLinks(nodeId: string) { private createSharedLinks(nodeId: string, sharedLinkWithExpirySettings?: SharedLinkBodyCreate) {
this.isDisabled = true; this.isDisabled = true;
this.sharedLinksApiService.createSharedLinks(nodeId).subscribe( this.sharedLinksApiService.createSharedLinks(nodeId, sharedLinkWithExpirySettings).subscribe(
(sharedLink: SharedLinkEntry) => { (sharedLink: SharedLinkEntry) => {
if (sharedLink.entry) { if (sharedLink.entry) {
this.sharedId = sharedLink.entry.id; this.sharedId = sharedLink.entry.id;
@ -215,7 +211,7 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
); );
} }
deleteSharedLink(sharedId: string) { deleteSharedLink(sharedId: string, dialogOpenFlag?: boolean) {
this.isDisabled = true; this.isDisabled = true;
this.sharedLinksApiService this.sharedLinksApiService
@ -230,9 +226,15 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
this.data.node.entry.properties['qshare:sharedId'] = null; this.data.node.entry.properties['qshare:sharedId'] = null;
this.data.node.entry.properties['qshare:expiryDate'] = null; this.data.node.entry.properties['qshare:expiryDate'] = null;
} }
if (dialogOpenFlag) {
this.createSharedLinks(this.data.node.entry.id);
this.isExpiryDateToggleChecked = false;
this.isLinkWithExpiryDate = false;
} else {
this.dialogRef.close(false); this.dialogRef.close(false);
} }
} }
}
); );
} }
@ -255,7 +257,7 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}); });
} }
private updateForm() { private updateForm(): Date {
const {entry} = this.data.node; const {entry} = this.data.node;
let expiryDate = null; let expiryDate = null;
@ -265,34 +267,55 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
this.form.setValue({ this.form.setValue({
sharedUrl: `${this.baseShareUrl}${this.sharedId}`, sharedUrl: `${this.baseShareUrl}${this.sharedId}`,
time: expiryDate ? moment(expiryDate).local() : null time: expiryDate ? new Date(expiryDate) : null
}); }, { emitEvent: false });
if (expiryDate) { return expiryDate;
this.time.enable(); }
private updateNode(date: Date) {
let expiryDate: Date | string;
if (date) {
if (this.type === 'date') {
expiryDate = format(endOfDay(new Date(date)), `yyyy-MM-dd'T'HH:mm:ss.SSSxx`);
} else { } else {
this.time.disable(); expiryDate = format((new Date(date)), `yyyy-MM-dd'T'HH:mm:ss.SSSxx`);
} }
} else {
expiryDate = null;
} }
private updateNode(date: moment.Moment): Observable<Node> { if (this.sharedId && expiryDate) {
const expiryDate = date this.isDisabled = true;
? (this.type === 'date' ? date.endOf('day').utc().format() : date.utc().format())
: null;
return this.nodesApiService.updateNode(this.data.node.entry.id, { this.sharedLinksApiService.deleteSharedLink(this.sharedId).subscribe((response: any) => {
properties: { if (response instanceof Error) {
'qshare:expiryDate': expiryDate this.isDisabled = false;
this.isFileShared = true;
this.handleError(response);
} else {
this.sharedLinkWithExpirySettings(expiryDate as Date);
this.isLinkWithExpiryDate = true;
this.updateEntryExpiryDate(date);
} }
}); });
} }
}
private updateEntryExpiryDate(date: moment.Moment) { private sharedLinkWithExpirySettings(expiryDate: Date) {
const nodeObject: SharedLinkBodyCreate = {
nodeId: this.data.node.entry.id,
expiresAt: expiryDate as Date
};
this.createSharedLinks(this.data.node.entry.id, nodeObject);
}
private updateEntryExpiryDate(date: Date) {
const {properties} = this.data.node.entry; const {properties} = this.data.node.entry;
if (properties) { if (properties) {
properties['qshare:expiryDate'] = date properties['qshare:expiryDate'] = date
? date.local() ? new Date(date)
: null; : null;
} }
} }

View File

@ -16,7 +16,7 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { NodePaging, SharedLinkEntry, SharedlinksApi } from '@alfresco/js-api'; import { NodePaging, SharedLinkBodyCreate, SharedLinkEntry, SharedlinksApi } from '@alfresco/js-api';
import { Observable, from, of, Subject } from 'rxjs'; import { Observable, from, of, Subject } from 'rxjs';
import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core'; import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@ -62,11 +62,12 @@ export class SharedLinksApiService {
* Creates a shared link available to the current user. * Creates a shared link available to the current user.
* *
* @param nodeId ID of the node to link to * @param nodeId ID of the node to link to
* @param sharedLinkWithExpirySettings shared link with nodeId and expiryDate
* @param options Options supported by JS-API * @param options Options supported by JS-API
* @returns The shared link just created * @returns The shared link just created
*/ */
createSharedLinks(nodeId: string, options: any = {}): Observable<SharedLinkEntry> { createSharedLinks(nodeId: string, sharedLinkWithExpirySettings?: SharedLinkBodyCreate, options: any = {}): Observable<SharedLinkEntry> {
const promise = this.sharedLinksApi.createSharedLink({ nodeId }, options); const promise = this.sharedLinksApi.createSharedLink(sharedLinkWithExpirySettings? sharedLinkWithExpirySettings : { nodeId }, options);
return from(promise).pipe( return from(promise).pipe(
catchError((err) => of(err)) catchError((err) => of(err))

View File

@ -456,8 +456,14 @@
"DIALOG-TITLE": "Share", "DIALOG-TITLE": "Share",
"DESCRIPTION": "Click the link below to copy it to the clipboard.", "DESCRIPTION": "Click the link below to copy it to the clipboard.",
"TITLE": "Link to share", "TITLE": "Link to share",
"SHARE-LINK": "Share Link",
"SHARABLE-LINK-CREATED": "Sharable Link is Created",
"PUBLIC-CONTENT": "This content is publicly available to anyone with this link",
"LINK-WITH-EXPIRY-SETTINGS": "New link has been generated with expiry settings",
"EXPIRES": "Expires on", "EXPIRES": "Expires on",
"LINK-EXPIRY-DATE": "Link Expiry Date",
"EXPIRATION-LABEL" : "Expiration Date", "EXPIRATION-LABEL" : "Expiration Date",
"EXPIRATION-PLACEHOLDER": "MM/DD/YYYY",
"CLIPBOARD-MESSAGE": "Link copied to the clipboard", "CLIPBOARD-MESSAGE": "Link copied to the clipboard",
"CLOSE": "Close", "CLOSE": "Close",
"COPY_BUTTON_LABEL": "Copy link", "COPY_BUTTON_LABEL": "Copy link",

View File

@ -21,8 +21,8 @@ import { BrowserActions } from '../../utils/browser-actions';
export class DateTimePickerCalendarPage { export class DateTimePickerCalendarPage {
datePicker = $(`.mat-datetimepicker-calendar`); datePicker = $(`.mat-datepicker-calendar`);
today = $(`.mat-datetimepicker-calendar-body-today`); today = $(`.mat-calendar-body-today`);
timePicker = $('.mat-datetimepicker-clock'); timePicker = $('.mat-datetimepicker-clock');
hourTime = $$('.mat-datetimepicker-clock-hours .mat-datetimepicker-clock-cell').first(); hourTime = $$('.mat-datetimepicker-clock-hours .mat-datetimepicker-clock-cell').first();
minutesTime = $$('.mat-datetimepicker-clock-minutes .mat-datetimepicker-clock-cell').first(); minutesTime = $$('.mat-datetimepicker-clock-minutes .mat-datetimepicker-clock-cell').first();
@ -47,7 +47,7 @@ export class DateTimePickerCalendarPage {
async setDate(date?: string): Promise<boolean> { async setDate(date?: string): Promise<boolean> {
try { try {
if (date) { if (date) {
await BrowserActions.clickScript(element.all(by.cssContainingText(`.mat-datetimepicker-calendar-body-cell-content`, date)).first()); await BrowserActions.clickScript(element.all(by.cssContainingText(`.mat-datepicker-calendar-body-cell-content`, date)).first());
} else { } else {
await this.setToday(); await this.setToday();
} }
@ -59,7 +59,7 @@ export class DateTimePickerCalendarPage {
} }
async checkCalendarTodayDayIsDisabled(): Promise<void> { async checkCalendarTodayDayIsDisabled(): Promise<void> {
await BrowserVisibility.waitUntilElementIsPresent(element(by.cssContainingText('.mat-datetimepicker-calendar-body-disabled', await BrowserActions.getText(this.today)))); await BrowserVisibility.waitUntilElementIsPresent(element(by.cssContainingText('.mat-calendar-body-cell-content', await BrowserActions.getText(this.today))));
} }
async setDefaultEnabledHour(): Promise<void> { async setDefaultEnabledHour(): Promise<void> {

View File

@ -23,12 +23,14 @@ export class DateTimePickerPage {
rootElement: ElementFinder; rootElement: ElementFinder;
dateTimePicker = $('.mat-datetimepicker-toggle'); dateTimePicker = $('.mat-datetimepicker-toggle');
datePicker = $('.mat-datepicker-toggle');
dateTime = new DateTimePickerCalendarPage(); dateTime = new DateTimePickerCalendarPage();
constructor(rootElement?: ElementFinder) { constructor(rootElement?: ElementFinder) {
if (rootElement) { if (rootElement) {
this.rootElement = rootElement; this.rootElement = rootElement;
this.dateTimePicker = this.rootElement.$('.mat-datetimepicker-toggle'); this.dateTimePicker = this.rootElement.$('.mat-datetimepicker-toggle');
this.datePicker = this.rootElement.$('.mat-datepicker-toggle');
} }
} }
@ -46,12 +48,12 @@ export class DateTimePickerPage {
} }
async setDate(date?: string): Promise<boolean> { async setDate(date?: string): Promise<boolean> {
await BrowserActions.click(this.dateTimePicker); await BrowserActions.click(this.datePicker);
return this.dateTime.setDate(date); return this.dateTime.setDate(date);
} }
async clickDateTimePicker(): Promise<void> { async clickDateTimePicker(): Promise<void> {
await BrowserActions.click(this.dateTimePicker); await BrowserActions.click(this.datePicker);
} }
async checkCalendarTodayDayIsDisabled(): Promise<void> { async checkCalendarTodayDayIsDisabled(): Promise<void> {

32
package-lock.json generated
View File

@ -36,6 +36,7 @@
"apollo-angular": "^4.2.1", "apollo-angular": "^4.2.1",
"chart.js": "2.9.4", "chart.js": "2.9.4",
"cropperjs": "1.5.13", "cropperjs": "1.5.13",
"date-fns": "^2.30.0",
"dotenv-expand": "^5.1.0", "dotenv-expand": "^5.1.0",
"editorjs-html": "3.4.2", "editorjs-html": "3.4.2",
"editorjs-paragraph-with-alignment": "3.0.0", "editorjs-paragraph-with-alignment": "3.0.0",
@ -30973,6 +30974,37 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/date-fns/node_modules/@babel/runtime": {
"version": "7.21.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz",
"integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/date-fns/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/date-format": { "node_modules/date-format": {
"version": "4.0.14", "version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",

View File

@ -80,6 +80,7 @@
"apollo-angular": "^4.2.1", "apollo-angular": "^4.2.1",
"chart.js": "2.9.4", "chart.js": "2.9.4",
"cropperjs": "1.5.13", "cropperjs": "1.5.13",
"date-fns": "^2.30.0",
"dotenv-expand": "^5.1.0", "dotenv-expand": "^5.1.0",
"editorjs-html": "3.4.2", "editorjs-html": "3.4.2",
"editorjs-paragraph-with-alignment": "3.0.0", "editorjs-paragraph-with-alignment": "3.0.0",