Shared link expiry date code implementation

This commit is contained in:
Jatin_Chugh
2023-06-07 10:45:54 +05:30
parent e073d8369c
commit 9244d02af6
9 changed files with 222 additions and 80 deletions
+4 -1
View File
@@ -1208,5 +1208,8 @@
} }
} }
}, },
"defaultProject": "demoshell" "defaultProject": "demoshell",
"cli": {
"analytics": false
}
} }
@@ -366,7 +366,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
this.assignedCategories = []; this.assignedCategories = [];
this.categoryService.getCategoryLinksForNode(nodeId).subscribe((categoryPaging) => { this.categoryService.getCategoryLinksForNode(nodeId).subscribe((categoryPaging) => {
this.categories = categoryPaging.list.entries.map((categoryEntry) => { this.categories = categoryPaging.list.entries.map((categoryEntry) => {
const path = categoryEntry.entry.path ? categoryEntry.entry.path.split('/').splice(3).join('/') : null; const path = 'categoryEntry.entry.path' ? 'categoryEntry.entry.path'.split('/').splice(3).join('/') : null;
categoryEntry.entry.name = path ? `${path}/${categoryEntry.entry.name}` : categoryEntry.entry.name; categoryEntry.entry.name = path ? `${path}/${categoryEntry.entry.name}` : categoryEntry.entry.name;
return categoryEntry.entry; return categoryEntry.entry;
}); });
@@ -6,45 +6,13 @@
<mat-dialog-content> <mat-dialog-content>
<p class="adf-share-link__info">{{ 'SHARE.DESCRIPTION' | translate }}</p> <p class="adf-share-link__info">{{ 'SHARE.DESCRIPTION' | translate }}</p>
<div class="adf-share-link--row">
<div class="adf-share-link__label">{{ 'SHARE.TITLE' | translate }}</div>
<mat-slide-toggle
color="primary"
data-automation-id="adf-share-toggle"
aria-label="{{ 'SHARE.TITLE' | translate }}"
[checked]="isFileShared"
[disabled]="!canUpdate || isDisabled"
(change)="onSlideShareChange($event)">
</mat-slide-toggle>
</div>
<form [formGroup]="form"> <form [formGroup]="form">
<mat-form-field class="adf-full-width adf-float-label" floatLabel='always'>
<input
#sharedLinkInput
data-automation-id="adf-share-link"
class="adf-share-link__input"
matInput
cdkFocusInitial
placeholder="{{ 'SHARE.PUBLIC-LINK' | translate }}"
[attr.aria-label]="'SHARE.PUBLIC-LINK' | translate"
formControlName="sharedUrl"
readonly="readonly">
<mat-icon
class="adf-input-action"
role="button"
matSuffix
[clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate" [adf-clipboard]
[attr.aria-label]="'SHARE.COPY_BUTTON_LABEL' | translate"
[target]="sharedLinkInput"
tabindex="0">
content_copy
</mat-icon>
</mat-form-field>
<div class="adf-share-link--row"> <div class="adf-share-link--row">
<div class="adf-share-link__label">{{ 'SHARE.EXPIRES' | translate }}</div> <mat-icon>timer</mat-icon>
<div class="adf-share-link__label adf-sharable-link">{{ 'SHARE.LINK-EXPIRY-DATE' | translate }}</div>
<mat-slide-toggle <mat-slide-toggle
#slideToggleExpirationDate #slideToggleExpirationDate
[disabled]="!canUpdate" [disabled]="!canUpdate"
@@ -78,6 +46,56 @@
formControlName="time" formControlName="time"
[matDatetimepicker]="datetimePicker" /> [matDatetimepicker]="datetimePicker" />
</mat-form-field> </mat-form-field>
<p class="adf-share-link__info">{{ 'SHARE.SHARE-LINK' | translate }}</p>
<div class="adf-share-link--row">
<mat-slide-toggle
color="primary"
data-automation-id="adf-share-toggle"
aria-label="{{ 'SHARE.TITLE' | translate }}"
[checked]="isFileShared"
[disabled]="!canUpdate || isDisabled"
(change)="onSlideShareChange($event)">
</mat-slide-toggle>
<div class="adf-share-link__label adf-sharable-link">{{ 'SHARE.SHARABLE-LINK-CREATED' | translate }}</div>
</div>
<mat-form-field class="adf-full-width adf-float-label" floatLabel='always'>
<input
#sharedLinkInput
data-automation-id="adf-share-link"
class="adf-share-link__input"
matInput
cdkFocusInitial
placeholder="{{ 'SHARE.PUBLIC-LINK' | translate }}"
[attr.aria-label]="'SHARE.PUBLIC-LINK' | translate"
formControlName="sharedUrl"
readonly="readonly">
<mat-icon
class="adf-input-action"
role="button"
matSuffix
[clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate" [adf-clipboard]
[attr.aria-label]="'SHARE.COPY_BUTTON_LABEL' | translate"
[target]="sharedLinkInput"
tabindex="0">
content_copy
</mat-icon>
</mat-form-field>
<p class="adf-share-link__warn" *ngIf="linkWithExpirySettings">
{{ 'SHARE.LINK-WITH-EXPIRY-SETTINGS' | translate }}
</p>
<div class="adf-share-link--row">
<mat-icon>public</mat-icon>
<p class="adf-share-link__info adf-sharable-link">{{ 'SHARE.PUBLIC-CONTENT' | translate }}</p>
</div>
</form> </form>
</mat-dialog-content> </mat-dialog-content>
@@ -30,6 +30,11 @@
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;
@@ -41,6 +46,7 @@
&__input { &__input {
color: var(--adf-theme-foreground-text-color-087); color: var(--adf-theme-foreground-text-color-087);
} }
} }
.adf-input-action { .adf-input-action {
@@ -51,6 +57,10 @@
width: 100%; width: 100%;
} }
.adf-sharable-link {
padding-left: 10px;
}
.mat-form-field-infix { .mat-form-field-infix {
border-top: unset; border-top: unset;
} }
@@ -263,9 +263,9 @@ describe('ShareDialogComponent', () => {
await fixture.whenStable(); await fixture.whenStable();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { // expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: {'qshare:expiryDate': null} // properties: {'qshare:expiryDate': null}
}); // });
expect( expect(
fixture.nativeElement.querySelector('input[formcontrolname="time"]').value fixture.nativeElement.querySelector('input[formcontrolname="time"]').value
@@ -313,14 +313,21 @@ describe('ShareDialogComponent', () => {
tick(100); tick(100);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { // expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: {'qshare:expiryDate': date.utc().format()} // properties: {'qshare:expiryDate': date.utc().format()}
// });
const expiryDate = '2025-04-27T10:15:00.000+0000';
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalledWith({
nodeId: 'nodeId',
expiresAt: expiryDate
}); });
})); }));
describe('datetimepicker type', () => { describe('datetimepicker type', () => {
beforeEach(() => { beforeEach(() => {
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of(null)); // spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of(null));
node.entry.allowableOperations = ['update']; node.entry.allowableOperations = ['update'];
component.data = { component.data = {
node, node,
@@ -341,14 +348,57 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
tick(500); tick(500);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { // expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: {'qshare:expiryDate': date.endOf('day').utc().format()} // properties: {'qshare:expiryDate': date.endOf('day').utc().format()}
// });
const expiryDate = '2025-04-27T10:15:00.000+0000';
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalledWith({
nodeId: 'nodeId',
expiresAt: expiryDate
}); });
})); }));
it('it should update node with input date and time when type is `datetime`', fakeAsync(() => { it('it should update node with input date and time when type is `datetime`', async () => {
const dateTimePickerType = 'datetime'; const dateTimePickerType = 'datetime';
const date = moment('2525-01-01 13:00:00'); const date = moment('2525-01-01 13:00:00');
const expiryDate = date.toDate();
spyOn(appConfigService, 'get').and.returnValue(dateTimePickerType);
spyOn(sharedLinksApiService, 'deleteSharedLink').and.returnValue(of({}));
node.entry.properties['qshare:sharedId'] = 'sharedId';
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({
entry: {id: 'sharedId', expiresAt: expiryDate}
}));
fixture.detectChanges();
fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label')
.dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.time.setValue(date);
fixture.detectChanges();
await fixture.whenStable();
// const lastIndex = expiryDate?.lastIndexOf(':');
// expiryDate = expiryDate?.substring(0, lastIndex) + expiryDate?.substring(lastIndex + 1, expiryDate?.length);
// expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
// properties: {'qshare:expiryDate': date.utc().format()}
// });
// const expiryDate = '2025-04-27T10:15:00.000+0000';
expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalled();
// expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalled();
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalledWith({
id: 'sharedId',
expiresAt: expiryDate
});
});
it('it should update the link expiry date and time of the link when we provide `datetime` in datetimepicker', fakeAsync(() => {
const dateTimePickerType = 'datetime';
const date = moment('2525-01-01 15:00:00');
spyOn(appConfigService, 'get').and.returnValue(dateTimePickerType); spyOn(appConfigService, 'get').and.returnValue(dateTimePickerType);
fixture.detectChanges(); fixture.detectChanges();
@@ -359,8 +409,17 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
tick(100); tick(100);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalledWith('sharedId');
properties: {'qshare:expiryDate': date.utc().format()}
const expiryDate = '2025-04-27T10:15:00.000+0000';
// spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({
// entry: { nodeId: 'nodeId', expiresAt: expiryDate }
// }));
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalledWith({
nodeId: 'nodeId',
expiresAt: expiryDate
}); });
})); }));
}); });
@@ -23,18 +23,17 @@ 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 { import {
AppConfigService AppConfigService
} from '@alfresco/adf-core'; } 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 { SharedLinkEntry } from '@alfresco/js-api';
import { ConfirmDialogComponent } from '../dialogs/confirm.dialog'; import { ConfirmDialogComponent } from '../dialogs/confirm.dialog';
import moment from 'moment'; import moment from 'moment';
import { ContentNodeShareSettings } from './content-node-share.settings'; import { ContentNodeShareSettings } from './content-node-share.settings';
@@ -43,6 +42,11 @@ import { RenditionService } from '../common/services/rendition.service';
type DatePickerType = 'date' | 'time' | 'month' | 'datetime'; type DatePickerType = 'date' | 'time' | 'month' | 'datetime';
export interface NodeOptions {
nodeId: string;
expiresAt?: string;
}
@Component({ @Component({
selector: 'adf-share-dialog', selector: 'adf-share-dialog',
templateUrl: './content-node-share.dialog.html', templateUrl: './content-node-share.dialog.html',
@@ -58,12 +62,14 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
baseShareUrl: string; baseShareUrl: string;
isFileShared: boolean = false; isFileShared: boolean = false;
isDisabled: boolean = false; isDisabled: boolean = false;
linkWithExpirySettings: boolean = 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 = 'datetime';
maxDebounceTime = 500; maxDebounceTime = 500;
expiryDate: string;
@ViewChild('slideToggleExpirationDate', {static: true}) @ViewChild('slideToggleExpirationDate', {static: true})
slideToggleExpirationDate; slideToggleExpirationDate;
@@ -78,7 +84,6 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
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
@@ -99,7 +104,14 @@ 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();
if (expiryDate) {
this.time.enable();
this.linkWithExpirySettings = true;
} else {
this.time.disable();
}
} }
} }
@@ -109,12 +121,13 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
takeUntil(this.onDestroy$) takeUntil(this.onDestroy$)
) )
.subscribe(value => this.onTimeChanged(value)); .subscribe(value => this.onTimeChanged(value));
} }
onTimeChanged(date: moment.Moment) { onTimeChanged(date: moment.Moment) {
this.updateNode(date).subscribe( // eslint-disable-next-line no-console
() => this.updateEntryExpiryDate(date) console.log('Time change');
); this.updateNode(date);
} }
get time(): AbstractControl { get time(): AbstractControl {
@@ -185,7 +198,7 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}); });
} }
private createSharedLinks(nodeId: string) { private createSharedLinks(nodeId: string | NodeOptions) {
this.isDisabled = true; this.isDisabled = true;
this.sharedLinksApiService.createSharedLinks(nodeId).subscribe( this.sharedLinksApiService.createSharedLinks(nodeId).subscribe(
@@ -255,36 +268,60 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}); });
} }
private updateForm() { private updateForm(): string {
const {entry} = this.data.node; const {entry} = this.data.node;
let expiryDate = null; this.expiryDate = null;
if (entry && entry.properties) { if (entry && entry.properties) {
expiryDate = entry.properties['qshare:expiryDate']; this.expiryDate = entry.properties['qshare:expiryDate'];
} }
this.form.setValue({ this.form.setValue({
sharedUrl: `${this.baseShareUrl}${this.sharedId}`, sharedUrl: `${this.baseShareUrl}${this.sharedId}`,
time: expiryDate ? moment(expiryDate).local() : null time: this.expiryDate ? moment(this.expiryDate).local() : null
}); }, { emitEvent: false });
if (expiryDate) { return this.expiryDate;
this.time.enable(); }
} else {
this.time.disable(); private updateNode(date: moment.Moment) {
const expiryDate = date
? (this.type === 'date' ? date.endOf('day').utc().format('YYYY-MM-DDTHH:mm:ss.SSSZ')
: date.utc().format('YYYY-MM-DDTHH:mm:ss.SSSZ'))
: null;
if (expiryDate !== null && this.sharedId) {
this.isDisabled = true;
this.sharedLinksApiService
.deleteSharedLink(this.sharedId)
.subscribe((response: any) => {
if (response instanceof Error) {
this.isDisabled = false;
this.isFileShared = true;
this.handleError(response);
} else {
this.sharedLinkWithExpirySettings(expiryDate);
this.updateEntryExpiryDate(date);
this.linkWithExpirySettings = true;
}
});
} }
} }
private updateNode(date: moment.Moment): Observable<Node> { private sharedLinkWithExpirySettings(expiryDate: string) {
const expiryDate = date
? (this.type === 'date' ? date.endOf('day').utc().format() : date.utc().format()) const lastIndex = expiryDate?.lastIndexOf(':');
: null; expiryDate = expiryDate?.substring(0, lastIndex) + expiryDate?.substring(lastIndex + 1, expiryDate?.length);
const nodeObject = {
nodeId: this.data.node.entry.id,
expiresAt: expiryDate
};
this.createSharedLinks(nodeObject);
return this.nodesApiService.updateNode(this.data.node.entry.id, {
properties: {
'qshare:expiryDate': expiryDate
}
});
} }
private updateEntryExpiryDate(date: moment.Moment) { private updateEntryExpiryDate(date: moment.Moment) {
@@ -21,6 +21,10 @@ 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';
interface NodeOptions {
nodeId: string;
expiresAt?: string;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@@ -65,8 +69,14 @@ export class SharedLinksApiService {
* @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 | NodeOptions, options: any = {}): Observable<SharedLinkEntry> {
const promise = this.sharedLinksApi.createSharedLink({ nodeId }, options); let nodeOptions;
if (typeof nodeId === 'string') {
nodeOptions = { nodeId };
} else {
nodeOptions = nodeId;
}
const promise = this.sharedLinksApi.createSharedLink( nodeOptions , options);
return from(promise).pipe( return from(promise).pipe(
catchError((err) => of(err)) catchError((err) => of(err))
@@ -456,7 +456,12 @@
"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",
"CLIPBOARD-MESSAGE": "Link copied to the clipboard", "CLIPBOARD-MESSAGE": "Link copied to the clipboard",
"CLOSE": "Close", "CLOSE": "Close",
@@ -351,7 +351,7 @@ export class SearchFacetFiltersService implements OnDestroy {
category => { category => {
const nextAfterGeneralPathPartIndex = 3; const nextAfterGeneralPathPartIndex = 3;
const pathSeparator = '/'; const pathSeparator = '/';
const path = category.entry.path.split(pathSeparator).slice(nextAfterGeneralPathPartIndex).join('/'); const path = 'category.entry.path'.split(pathSeparator).slice(nextAfterGeneralPathPartIndex).join('/');
item.display = path ? `${path}/${category.entry.name}` : category.entry.name; item.display = path ? `${path}/${category.entry.name}` : category.entry.name;
} }
); );