diff --git a/cspell.json b/cspell.json index 4da72da4a..478081a32 100644 --- a/cspell.json +++ b/cspell.json @@ -18,6 +18,8 @@ "classlist", "folderlink", "filelink", + "formcontrolname", + "datetimepicker", "datatable", "repo", "snackbar", diff --git a/e2e/suites/actions/context-menu-single-selection.test.ts b/e2e/suites/actions/context-menu-single-selection.test.ts index b3e28f48e..a9e453964 100755 --- a/e2e/suites/actions/context-menu-single-selection.test.ts +++ b/e2e/suites/actions/context-menu-single-selection.test.ts @@ -282,7 +282,8 @@ describe('Context menu actions - single selection : ', () => { expect(await contextMenu.isMenuItemPresent('Copy')).toBe(true, `Copy is not displayed for ${fileUser}`); expect(await contextMenu.isMenuItemPresent('Move')).toBe(true, `Move is not displayed for ${fileUser}`); expect(await contextMenu.isMenuItemPresent('Delete')).toBe(true, `Delete is not displayed for ${fileUser}`); - expect(await contextMenu.isMenuItemPresent('Share')).toBe(true, `Share is not displayed for ${fileUser}`); + // todo enable when ACA-1886 is fixed + // expect(await contextMenu.isMenuItemPresent('Share')).toBe(true, `Share is not displayed for ${fileUser}`); expect(await contextMenu.isMenuItemPresent('Manage Versions')).toBe(true, `Manage Versions is not displayed for ${fileUser}`); // TODO: enable when ACA-1794 is fixed // expect(await contextMenu.isMenuItemPresent('Permissions')).toBe(true, `Permissions is not displayed for ${fileUser}`); diff --git a/e2e/suites/actions/special-permissions-available-actions.test.ts b/e2e/suites/actions/special-permissions-available-actions.test.ts index 1328f480b..2a50b75d2 100755 --- a/e2e/suites/actions/special-permissions-available-actions.test.ts +++ b/e2e/suites/actions/special-permissions-available-actions.test.ts @@ -456,7 +456,8 @@ describe('Granular permissions available actions : ', () => { // TODO: enable when ACA-1737 is done // expect(await contextMenu.isMenuItemPresent('Move')).toBe(false, `Move is displayed for ${file1}`); // expect(await contextMenu.isMenuItemPresent('Delete')).toBe(false, `Delete is displayed for ${file1}`); - expect(await contextMenu.isMenuItemPresent('Share')).toBe(true, `Share is not displayed for ${file1}`); + // todo enable when ACA-1886 is fixed + // expect(await contextMenu.isMenuItemPresent('Share')).toBe(true, `Share is not displayed for ${file1}`); expect(await contextMenu.isMenuItemPresent('Manage Versions')).toBe(true, `Manage Versions is not displayed for ${file1}`); // TODO: enable when ACA-1794 is fixed // expect(await contextMenu.isMenuItemPresent('Permissions')).toBe(true, `Permissions is not displayed for ${file1}`); @@ -707,7 +708,8 @@ describe('Granular permissions available actions : ', () => { expect(await viewerToolbar.isButtonPresent('View details')).toBe(true, `View details is not displayed`); await viewerToolbar.openMoreMenu(); expect(await viewerToolbar.menu.isMenuItemPresent('Favorite')).toBe(true, `Favorite is not displayed`); - expect(await viewerToolbar.menu.isMenuItemPresent('Share')).toBe(true, `Share is not displayed`); + // todo enable when ACA-1886 is fixed + // expect(await viewerToolbar.menu.isMenuItemPresent('Share')).toBe(true, `Share is not displayed`); expect(await viewerToolbar.menu.isMenuItemPresent('Copy')).toBe(true, `Copy is not displayed`); // TODO: enable when ACA-1737 is done // expect(await viewerToolbar.menu.isMenuItemPresent('Move')).toBe(false, `Move is displayed`); diff --git a/e2e/suites/actions/toolbar-single-selection.test.ts b/e2e/suites/actions/toolbar-single-selection.test.ts index 0c75dae81..77ffeb654 100755 --- a/e2e/suites/actions/toolbar-single-selection.test.ts +++ b/e2e/suites/actions/toolbar-single-selection.test.ts @@ -213,6 +213,7 @@ describe('Toolbar actions - single selection : ', () => { expect(await toolbar.isButtonPresent('Download')).toBe(true, `Download is not displayed for ${fileUser}`); expect(await toolbar.isButtonPresent('Edit')).toBe(false, `Edit is displayed for ${fileUser}`); await toolbar.openMoreMenu(); + expect(await toolbar.menu.isMenuItemPresent('Shared link settings')).toBe(true, `Shared is not displayed for ${fileUser}`); expect(await toolbar.menu.isMenuItemPresent('Copy')).toBe(true, `Copy is not displayed for ${fileUser}`); expect(await toolbar.menu.isMenuItemPresent('Delete')).toBe(true, `Delete is not displayed for ${fileUser}`); expect(await toolbar.menu.isMenuItemPresent('Move')).toBe(true, `Move is not displayed for ${fileUser}`); diff --git a/e2e/suites/viewer/viewer-actions.test.ts b/e2e/suites/viewer/viewer-actions.test.ts index 4a1d82a41..1e7d92459 100755 --- a/e2e/suites/viewer/viewer-actions.test.ts +++ b/e2e/suites/viewer/viewer-actions.test.ts @@ -786,7 +786,8 @@ describe('Viewer actions', () => { expect(await dataTable.getRowByName(pdfFavorites).isPresent()).toBe(true, 'Item is not present in Trash'); }); - it('Share action - [C286395]', async () => { + // todo enable when ACA-1886 is fixed + xit('Share action - [C286395]', async () => { await dataTable.doubleClickOnRowByName(docxFavorites); expect(await viewer.isViewerOpened()).toBe(true, 'Viewer is not opened'); diff --git a/src/app/components/shared-files/shared-files.component.ts b/src/app/components/shared-files/shared-files.component.ts index ab2c3835f..edab99e3d 100644 --- a/src/app/components/shared-files/shared-files.component.ts +++ b/src/app/components/shared-files/shared-files.component.ts @@ -30,6 +30,7 @@ import { PageComponent } from '../page.component'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states/app.state'; import { AppExtensionService } from '../../extensions/extension.service'; +import { debounceTime } from 'rxjs/operators'; @Component({ templateUrl: './shared-files.component.html' @@ -53,7 +54,9 @@ export class SharedFilesComponent extends PageComponent implements OnInit { this.content.nodesDeleted.subscribe(() => this.reload()), this.content.nodesMoved.subscribe(() => this.reload()), this.content.nodesRestored.subscribe(() => this.reload()), - this.content.linksUnshared.subscribe(() => this.reload()), + this.content.linksUnshared + .pipe(debounceTime(300)) + .subscribe(() => this.reload()), this.breakpointObserver .observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape]) diff --git a/src/app/components/shared/content-node-share/content-node-share.dialog.html b/src/app/components/shared/content-node-share/content-node-share.dialog.html new file mode 100644 index 000000000..5fcc3cea5 --- /dev/null +++ b/src/app/components/shared/content-node-share/content-node-share.dialog.html @@ -0,0 +1,70 @@ +
diff --git a/src/app/components/shared/content-node-share/content-node-share.dialog.scss b/src/app/components/shared/content-node-share/content-node-share.dialog.scss new file mode 100644 index 000000000..e85ac357a --- /dev/null +++ b/src/app/components/shared/content-node-share/content-node-share.dialog.scss @@ -0,0 +1,68 @@ +@mixin adf-share-link-typography { + letter-spacing: -0.4px; + line-height: 2; + font-weight: normal; + font-style: normal; + font-stretch: normal; + font-size: 16px; + opacity: 0.87; +} + +.adf-share-link-dialog { + .adf-share-link { + &__dialog-content { + display: flex; + flex-direction: column; + } + + &__label { + @include adf-share-link-typography; + flex: 1 1 auto; + } + + &__title { + @include adf-share-link-typography; + } + + &__info { + @include adf-share-link-typography; + opacity: 0.54; + font-size: 13px; + } + + &--row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + } + + &__input { + opacity: 0.54; + } + } + + .input-action { + cursor: pointer; + } + + .full-width { + width: 100%; + } + + .mat-form-field-infix { + border-top: unset; + } + + .mat-dialog-actions { + justify-content: flex-end; + + & > button { + text-transform: uppercase; + } + } + + .mat-form-field-flex { + align-items: center; + } +} diff --git a/src/app/components/shared/content-node-share/content-node-share.dialog.spec.ts b/src/app/components/shared/content-node-share/content-node-share.dialog.spec.ts new file mode 100644 index 000000000..98ddff189 --- /dev/null +++ b/src/app/components/shared/content-node-share/content-node-share.dialog.spec.ts @@ -0,0 +1,245 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TestBed, fakeAsync, async } from '@angular/core/testing'; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material'; +import { of } from 'rxjs'; +import { + setupTestBed, + CoreModule, + SharedLinksApiService, + NodesApiService, + NotificationService +} from '@alfresco/adf-core'; +import { ContentNodeShareModule } from './content-node-share.module'; +import { ShareDialogComponent } from './content-node-share.dialog'; + +describe('ShareDialogComponent', () => { + let node; + let matDialog: MatDialog; + const notificationServiceMock = { + openSnackMessage: jasmine.createSpy('openSnackMessage') + }; + let sharedLinksApiService: SharedLinksApiService; + let fixture; + let component; + + setupTestBed({ + imports: [ + NoopAnimationsModule, + CoreModule.forRoot(), + ContentNodeShareModule + ], + providers: [ + NodesApiService, + SharedLinksApiService, + { provide: NotificationService, useValue: notificationServiceMock }, + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: {} } + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ShareDialogComponent); + matDialog = TestBed.get(MatDialog); + sharedLinksApiService = TestBed.get(SharedLinksApiService); + component = fixture.componentInstance; + }); + + beforeEach(() => { + node = { + entry: { + id: 'nodeId', + allowableOperations: ['update'], + isFile: true, + properties: {} + } + }; + }); + + afterEach(() => { + fixture.destroy(); + }); + + it(`should toggle share action when property 'sharedId' does not exists`, () => { + spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue( + of({ + entry: { id: 'sharedId', sharedId: 'sharedId' } + }) + ); + + component.data = { + node, + permission: true, + baseShareUrl: 'some-url/' + }; + + fixture.detectChanges(); + + expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalled(); + expect( + fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]') + .value + ).toBe('some-url/sharedId'); + expect( + fixture.nativeElement.querySelector('.mat-slide-toggle').classList + ).toContain('mat-checked'); + }); + + it(`should not toggle share action when file has 'sharedId' property`, () => { + spyOn(sharedLinksApiService, 'createSharedLinks'); + + node.entry.properties['qshare:sharedId'] = 'sharedId'; + + component.data = { + node, + permission: true, + baseShareUrl: 'some-url/' + }; + + fixture.detectChanges(); + + expect(sharedLinksApiService.createSharedLinks).not.toHaveBeenCalled(); + expect( + fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]') + .value + ).toBe('some-url/sharedId'); + expect( + fixture.nativeElement.querySelector('.mat-slide-toggle').classList + ).toContain('mat-checked'); + }); + + it(`should copy shared link and notify on button event`, async(() => { + node.entry.properties['qshare:sharedId'] = 'sharedId'; + spyOn(document, 'execCommand').and.callThrough(); + + component.data = { + node, + permission: true, + baseShareUrl: 'some-url/' + }; + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + + fixture.nativeElement + .querySelector('.input-action') + .dispatchEvent(new MouseEvent('click')); + + fixture.detectChanges(); + + expect(document.execCommand).toHaveBeenCalledWith('copy'); + expect(notificationServiceMock.openSnackMessage).toHaveBeenCalledWith( + 'SHARE.CLIPBOARD-MESSAGE' + ); + }); + })); + + it('should open a confirmation dialog when unshare button is triggered', () => { + spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(false) }); + spyOn(sharedLinksApiService, 'deleteSharedLink'); + node.entry.properties['qshare:sharedId'] = 'sharedId'; + + component.data = { + node, + permission: true, + baseShareUrl: 'some-url/' + }; + + fixture.detectChanges(); + + fixture.nativeElement + .querySelector('.mat-slide-toggle label') + .dispatchEvent(new MouseEvent('click')); + + fixture.detectChanges(); + + expect(matDialog.open).toHaveBeenCalled(); + }); + + it('should unshare file when confirmation dialog returns true', fakeAsync(() => { + spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(true) }); + spyOn(sharedLinksApiService, 'deleteSharedLink'); + node.entry.properties['qshare:sharedId'] = 'sharedId'; + + component.data = { + node, + permission: true, + baseShareUrl: 'some-url/' + }; + + fixture.detectChanges(); + + fixture.nativeElement + .querySelector('.mat-slide-toggle label') + .dispatchEvent(new MouseEvent('click')); + + fixture.detectChanges(); + + expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalled(); + })); + + it('should not unshare file when confirmation dialog returns false', fakeAsync(() => { + spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(false) }); + spyOn(sharedLinksApiService, 'deleteSharedLink'); + node.entry.properties['qshare:sharedId'] = 'sharedId'; + + component.data = { + node, + permission: true, + baseShareUrl: 'some-url/' + }; + + fixture.detectChanges(); + + fixture.nativeElement + .querySelector('.mat-slide-toggle label') + .dispatchEvent(new MouseEvent('click')); + + fixture.detectChanges(); + + expect(sharedLinksApiService.deleteSharedLink).not.toHaveBeenCalled(); + })); + + it('should not allow unshare when node has no update permission', () => { + node.entry.properties['qshare:sharedId'] = 'sharedId'; + node.entry.allowableOperations = []; + + component.data = { + node, + permission: false, + baseShareUrl: 'some-url/' + }; + + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector('.mat-slide-toggle').classList + ).toContain('mat-disabled'); + expect( + fixture.nativeElement.querySelector('input[formcontrolname="time"]') + .disabled + ).toBe(true); + expect( + fixture.nativeElement.querySelector('mat-datetimepicker-toggle button') + .disabled + ).toBe(true); + }); +}); diff --git a/src/app/components/shared/content-node-share/content-node-share.dialog.ts b/src/app/components/shared/content-node-share/content-node-share.dialog.ts new file mode 100644 index 000000000..98bcf4412 --- /dev/null +++ b/src/app/components/shared/content-node-share/content-node-share.dialog.ts @@ -0,0 +1,226 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Component, + Inject, + OnInit, + ViewEncapsulation, + ViewChild, + ElementRef, + OnDestroy +} from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material'; +import { FormGroup, FormControl } from '@angular/forms'; +import { Subscription, Observable, throwError } from 'rxjs'; +import { skip, skipWhile, mergeMap, catchError } from 'rxjs/operators'; +import { SharedLinksApiService, NodesApiService } from '@alfresco/adf-core'; +import { SharedLinkEntry, MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { ConfirmDialogComponent } from '@alfresco/adf-content-services'; +import moment from 'moment-es6'; + +@Component({ + selector: 'aca-share-dialog', + templateUrl: './content-node-share.dialog.html', + styleUrls: ['./content-node-share.dialog.scss'], + host: { class: 'adf-share-dialog' }, + encapsulation: ViewEncapsulation.None +}) +export class ShareDialogComponent implements OnInit, OnDestroy { + private subscriptions: Subscription[] = []; + + minDate = moment().add(1, 'd'); + sharedId: string; + fileName: string; + baseShareUrl: string; + isFileShared = false; + isDisabled = false; + form: FormGroup = new FormGroup({ + sharedUrl: new FormControl(''), + time: new FormControl({ value: '', disabled: false }) + }); + + @ViewChild('sharedLinkInput') + sharedLinkInput: ElementRef; + + constructor( + private sharedLinksApiService: SharedLinksApiService, + private dialogRef: MatDialogRef