From 513915b3d9653ba22c9efd57d0a13837fcf7bcaf Mon Sep 17 00:00:00 2001 From: Vito Date: Thu, 3 May 2018 15:14:15 +0100 Subject: [PATCH] [ADF-2556] Created component for add group or user to permission (#3242) * [ADF-2556] first step to create add people or group to permissions * [ADF-2556] creating a dialog with user results * [ADF-2556] integrated service for add and remove permission from node * [ADF-2556] fixed behaviour and style for add user group * [ADF-2556] added some refactoring for dialog service * [ADF-2556] refactoring the dependencies of the components * [ADF-2556] added some fix and a new key for dialog * [ADF-2556] start adding test for node permission service * [ADF-2556] added test for add permission panel component * [ADf-2556] adding tests for new add permission component * [ADF-2556] fixed tests and added documentation * [ADF-2556] fixed documentation for add-node components * [ADF-2556] added peer review changes --- .../demo-permissions.component.html | 8 +- .../demo-permissions.component.scss | 2 +- .../permissions/demo-permissions.component.ts | 28 +++- .../add-permission-dialog.component.md | 63 +++++++ .../add-permission-panel.component.md | 37 +++++ .../add-permission.component.md | 41 +++++ .../images/add-permission-component.png | Bin 0 -> 42865 bytes lib/content-services/i18n/en.json | 10 +- .../mock/add-permission.component.mock.ts | 153 +++++++++++++++++ .../mock/permission-list.component.mock.ts | 83 ++++++++++ .../add-permission-dialog-data.interface.ts | 25 +++ .../add-permission-dialog.component.html | 16 ++ .../add-permission-dialog.component.scss | 55 ++++++ .../add-permission-dialog.component.spec.ts | 111 +++++++++++++ .../add-permission-dialog.component.ts | 48 ++++++ .../add-permission-panel.component.html | 54 ++++++ .../add-permission-panel.component.scss | 68 ++++++++ .../add-permission-panel.component.spec.ts | 156 ++++++++++++++++++ .../add-permission-panel.component.ts | 82 +++++++++ .../add-permission.component.html | 14 ++ .../add-permission.component.scss | 16 ++ .../add-permission.component.spec.ts | 102 ++++++++++++ .../add-permission.component.ts | 61 +++++++ .../search-config-permission.service.ts | 43 +++++ .../inherited-button.directive.spec.ts | 10 +- .../components/inherited-button.directive.ts | 9 +- .../permission-list.component.html | 7 + .../permission-list.component.ts | 11 +- .../models/permission.model.ts | 2 +- .../permission-manager.module.ts | 20 ++- .../permission-manager/public-api.ts | 7 +- .../node-permission-dialog.service.spec.ts | 83 ++++++++++ .../node-permission-dialog.service.ts | 64 +++++++ .../services/node-permission.service.spec.ts | 57 ++++++- .../services/node-permission.service.ts | 47 +++++- .../components/search-trigger.directive.ts | 2 +- lib/content-services/styles/_index.scss | 6 + 37 files changed, 1576 insertions(+), 25 deletions(-) create mode 100644 docs/content-services/add-permission-dialog.component.md create mode 100644 docs/content-services/add-permission-panel.component.md create mode 100644 docs/content-services/add-permission.component.md create mode 100644 docs/docassets/images/add-permission-component.png create mode 100644 lib/content-services/mock/add-permission.component.mock.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-dialog-data.interface.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.html create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.scss create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.html create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.scss create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.spec.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission.component.html create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission.component.scss create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission.component.spec.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/add-permission.component.ts create mode 100644 lib/content-services/permission-manager/components/add-permission/search-config-permission.service.ts create mode 100644 lib/content-services/permission-manager/services/node-permission-dialog.service.spec.ts create mode 100644 lib/content-services/permission-manager/services/node-permission-dialog.service.ts diff --git a/demo-shell/src/app/components/permissions/demo-permissions.component.html b/demo-shell/src/app/components/permissions/demo-permissions.component.html index 2adeb22178..6cb86d301d 100644 --- a/demo-shell/src/app/components/permissions/demo-permissions.component.html +++ b/demo-shell/src/app/components/permissions/demo-permissions.component.html @@ -2,11 +2,17 @@ + +
- +
diff --git a/demo-shell/src/app/components/permissions/demo-permissions.component.scss b/demo-shell/src/app/components/permissions/demo-permissions.component.scss index a6da03dab0..18a2c1fa49 100644 --- a/demo-shell/src/app/components/permissions/demo-permissions.component.scss +++ b/demo-shell/src/app/components/permissions/demo-permissions.component.scss @@ -1,6 +1,6 @@ .inherit_permission_button { padding-top: 20px; display: flex; - justify-content: space-evenly; + justify-content: center; padding-bottom: 20px; } diff --git a/demo-shell/src/app/components/permissions/demo-permissions.component.ts b/demo-shell/src/app/components/permissions/demo-permissions.component.ts index cc4ad25a3e..49455b8c51 100644 --- a/demo-shell/src/app/components/permissions/demo-permissions.component.ts +++ b/demo-shell/src/app/components/permissions/demo-permissions.component.ts @@ -17,9 +17,9 @@ import { Component, Optional, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Params} from '@angular/router'; -import { PermissionListComponent } from '@alfresco/adf-content-services'; +import { PermissionListComponent, NodePermissionDialogService } from '@alfresco/adf-content-services'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { NodesApiService } from '@alfresco/adf-core'; +import { NodesApiService, NotificationService } from '@alfresco/adf-core'; @Component({ selector: 'app-permissions', @@ -35,7 +35,9 @@ export class DemoPermissionComponent implements OnInit { toggleStatus = false; constructor(@Optional() private route: ActivatedRoute, - private nodeService: NodesApiService) { + private nodeService: NodesApiService, + private nodePermissionDialogService: NodePermissionDialogService, + private notificationService: NotificationService) { } ngOnInit() { @@ -56,4 +58,24 @@ export class DemoPermissionComponent implements OnInit { this.displayPermissionComponent.reload(); } + reloadList() { + this.displayPermissionComponent.reload(); + } + + openAddPermissionDialog(event: Event) { + this.nodePermissionDialogService.updateNodePermissionByDialog(this.nodeId).subscribe(() => { + this.displayPermissionComponent.reload(); + }, + (error) => { + this.showErrorMessage(error); + }); + } + + showErrorMessage(error) { + this.notificationService.openSnackMessage( + JSON.parse(error.response.text).error.errorKey, + 4000 + ); + } + } diff --git a/docs/content-services/add-permission-dialog.component.md b/docs/content-services/add-permission-dialog.component.md new file mode 100644 index 0000000000..1a5daaa4f1 --- /dev/null +++ b/docs/content-services/add-permission-dialog.component.md @@ -0,0 +1,63 @@ +--- +Added: v2.4.0 +Status: Active +Last reviewed: 2018-05-03 +--- + +# Add Permission Dialog Component + +Allow user to search people or group that could be added to the current node permissions. + +![Add Permission Component](../docassets/images/add-permission-component.png) + +## Basic Usage + +```ts +import { NodePermissionDialogService } from '@alfresco/adf-content-services'; + + constructor(private nodePermissionDialogService: nodePermissionDialogService) { + } + + this.nodePermissionDialogService.openAddPermissionDialog(this.nodeId).subscribe((selectedNodes) => { + //action for selected nodes + }, + (error) => { + this.showErrorMessage(error); + }); +``` + +## Class members + +### Properties + +| Name | Type | Default value | Description | +| -- | -- | -- | -- | +| nodeId | `string` | "" | | + +### Events + +| Name | Type | Description | +| -- | -- | -- | +| success | `EventEmitter` | | +| error | `EventEmitter` | | + +## Details + +This component extends the [Add permission panel component](../add-permission-panel.component.md) +and apply the action confirm when the selection made is accepted. +The dialog will be opened via the nodePermissionDialogService which will provide an Observable to subscribe to for getting the node selected. +In case you want the dialog service to take care of update the current node you can call `updateNodePermissionByDialog` in this way : + +```ts +import { NodePermissionDialogService } from '@alfresco/adf-content-services'; + + constructor(private nodePermissionDialogService: nodePermissionDialogService) { + } + + this.nodePermissionDialogService.updateNodePermissionByDialog(this.nodeId).subscribe((node) => { + //updated node + }, + (error) => { + this.showErrorMessage(error); + }); +``` \ No newline at end of file diff --git a/docs/content-services/add-permission-panel.component.md b/docs/content-services/add-permission-panel.component.md new file mode 100644 index 0000000000..479e7bd0bb --- /dev/null +++ b/docs/content-services/add-permission-panel.component.md @@ -0,0 +1,37 @@ +--- +Added: v2.4.0 +Status: Active +Last reviewed: 2018-05-03 +--- + +# Add Permission Component + +Allow user to search people or group that could be added to the current node permissions. + +![Add Permission Component](../docassets/images/add-permission-component.png) + +## Basic Usage + +```html + + +``` + +## Class members + +### Properties + +| Name | Type | Default value | Description | +| -- | -- | -- | -- | + +### Events + +| Name | Type | Description | +| -- | -- | -- | +| select | `EventEmitter` | | + +## Details +This component uses a [Search component](../search.component.md) to retrieve the +groups and people that could be added to the permission list of the current node. +The `select` event will be emitted when a result is clicked from the list. \ No newline at end of file diff --git a/docs/content-services/add-permission.component.md b/docs/content-services/add-permission.component.md new file mode 100644 index 0000000000..f86e89bd2a --- /dev/null +++ b/docs/content-services/add-permission.component.md @@ -0,0 +1,41 @@ +--- +Added: v2.4.0 +Status: Active +Last reviewed: 2018-05-03 +--- + +# Add Permission Component + +Allow user to search people or group that could be added to the current node permissions. + +![Add Permission Component](../docassets/images/add-permission-component.png) + +## Basic Usage + +```html + + +``` + +## Class members + +### Properties + +| Name | Type | Default value | Description | +| -- | -- | -- | -- | +| nodeId | `string` | "" | | + +### Events + +| Name | Type | Description | +| -- | -- | -- | +| success | `EventEmitter` | | +| error | `EventEmitter` | | + +## Details + +This component extends the [Add permission panel component](../add-permission-panel.component.md) +and apply the action confirm when the selection made is accepted. +The `success` event will be emitted when the node is correctly updated. +The `error` event will be thrown whenever the node update permission will fail. diff --git a/docs/docassets/images/add-permission-component.png b/docs/docassets/images/add-permission-component.png new file mode 100644 index 0000000000000000000000000000000000000000..b3a9628f85c8d4c76d436f57ba82a742e48f43da GIT binary patch literal 42865 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 +} diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.html b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.html new file mode 100644 index 0000000000..0a6891632d --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.html @@ -0,0 +1,16 @@ +

+ {{(data?.title ? data?.title : 'PERMISSION_MANAGER.ADD-PERMISSION.BASE-DIALOG-TITLE') | translate}} +

+ + + + + + + + + diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.scss b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.scss new file mode 100644 index 0000000000..457536fef1 --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.scss @@ -0,0 +1,55 @@ +@mixin adf-add-permission-dialog-theme($theme) { + + $primary: map-get($theme, primary); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + + .adf-add-permission-dialog { + + .mat-dialog-title { + margin-left: 24px; + margin-right: 24px; + font-size: 20px; + font-weight: 600; + font-style: normal; + font-stretch: normal; + line-height: 1.6; + letter-spacing: -0.5px; + color: mat-color($foreground, text, 0.87); + } + + .mat-dialog-container { + padding-left: 0; + padding-right: 0; + } + + .mat-dialog-content { + margin: 0; + overflow: hidden; + } + + .mat-dialog-actions { + padding: 8px; + background-color: mat-color($background, background); + display: flex; + justify-content: flex-end; + color: mat-color($foreground, secondary-text); + + button { + text-transform: uppercase; + font-weight: normal; + } + + .choose-action { + + &[disabled] { + opacity: 0.6; + } + + &:enabled { + color: mat-color($primary); + } + } + } + } +} diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts new file mode 100644 index 0000000000..b523e751d4 --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts @@ -0,0 +1,111 @@ +/*! + * @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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { By } from '@angular/platform-browser'; +import { setupTestBed } from '@alfresco/adf-core'; +import { AddPermissionDialogComponent } from './add-permission-dialog.component'; +import { MinimalNodeEntity } from 'alfresco-js-api'; +import { Subject } from 'rxjs/Subject'; +import { AddPermissionDialogData } from './add-permission-dialog-data.interface'; +import { fakeAuthorityResults } from '../../../mock/add-permission.component.mock'; +import { AddPermissionPanelComponent } from './add-permission-panel.component'; + +describe('AddPermissionDialog', () => { + + let fixture: ComponentFixture; + let element: HTMLElement; + let data: AddPermissionDialogData = { + title: 'dead or alive you are coming with me', + nodeId: 'fake-node-id', + confirm: new Subject () + }; + const dialogRef = { + close: jasmine.createSpy('close') + }; + + setupTestBed({ + imports: [ContentTestingModule], + providers: [ + { provide: MatDialogRef, useValue: dialogRef }, + { provide: MAT_DIALOG_DATA, useValue: data } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + + beforeEach(() => { + + fixture = TestBed.createComponent(AddPermissionDialogComponent); + element = fixture.nativeElement; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it('should show the INJECTED title', () => { + const titleElement = fixture.debugElement.query(By.css('#add-permission-dialog-title')); + expect(titleElement).not.toBeNull(); + expect(titleElement.nativeElement.innerText).toBe('dead or alive you are coming with me'); + }); + + it('should close the dialog when close button is clicked', () => { + const closeButton: HTMLButtonElement = element.querySelector('#add-permission-dialog-close-button'); + expect(closeButton).not.toBeNull(); + closeButton.click(); + expect(dialogRef.close).toHaveBeenCalled(); + }); + + it('should disable the confirm button when no selection is applied', () => { + const confirmButton: HTMLButtonElement = element.querySelector('#add-permission-dialog-confirm-button'); + expect(confirmButton.disabled).toBeTruthy(); + }); + + it('should enable the button when a selection is done', async(() => { + const addPermissionPanelComponent: AddPermissionPanelComponent = fixture.debugElement.query(By.directive(AddPermissionPanelComponent)).componentInstance; + addPermissionPanelComponent.select.emit(fakeAuthorityResults); + let confirmButton: HTMLButtonElement = element.querySelector('#add-permission-dialog-confirm-button'); + expect(confirmButton.disabled).toBeTruthy(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + confirmButton = element.querySelector('#add-permission-dialog-confirm-button'); + expect(confirmButton.disabled).toBeFalsy(); + }); + })); + + it('should stream the confirmed selection on the confirm subject', async(() => { + const addPermissionPanelComponent: AddPermissionPanelComponent = fixture.debugElement.query(By.directive(AddPermissionPanelComponent)).componentInstance; + addPermissionPanelComponent.select.emit(fakeAuthorityResults); + data.confirm.subscribe((selection) => { + expect(selection[0]).not.toBeNull(); + expect(selection[0].entry.id).not.toBeNull(); + expect(fakeAuthorityResults[0].entry.id).toBe(selection[0].entry.id); + }); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const confirmButton = element.querySelector('#add-permission-dialog-confirm-button'); + confirmButton.click(); + }); + })); +}); diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.ts b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.ts new file mode 100644 index 0000000000..c41d208557 --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-dialog.component.ts @@ -0,0 +1,48 @@ +/*! + * @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, ViewEncapsulation, Inject, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material'; +import { MinimalNodeEntity } from 'alfresco-js-api'; +import { AddPermissionDialogData } from './add-permission-dialog-data.interface'; +import { AddPermissionComponent } from '../add-permission/add-permission.component'; + +@Component({ + selector: 'adf-add-permission-dialog', + templateUrl: './add-permission-dialog.component.html', + styleUrls: ['./add-permission-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AddPermissionDialogComponent { + + @ViewChild('addPermission') + addPermissionComponent: AddPermissionComponent; + + private currentSelection: MinimalNodeEntity[] = []; + + constructor(@Inject(MAT_DIALOG_DATA) public data: AddPermissionDialogData) { + } + + onSelect(items: MinimalNodeEntity[]) { + this.currentSelection = items; + } + + onAddClicked() { + this.data.confirm.next(this.currentSelection); + this.data.confirm.complete(); + } +} diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.html b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.html new file mode 100644 index 0000000000..801c16b3de --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.html @@ -0,0 +1,54 @@ + + + + clear + + + search + + + + + + + + + + + group_add + + + person_add + +

+ {{item.entry?.properties['cm:authorityName']? + item.entry?.properties['cm:authorityName'] : + item.entry?.properties['cm:firstName']}}

+
+
+
+ {{'PERMISSION_MANAGER.ADD-PERMISSION.NO-RESULT' | translate}} +
+
+
diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.scss b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.scss new file mode 100644 index 0000000000..10e293d7ac --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.scss @@ -0,0 +1,68 @@ +@mixin adf-add-permission-panel-theme($theme) { + + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $mat-menu-border-radius: 2px !default; + + .adf { + + &-permission-result-list { + display: flex; + height: 300px; + overflow: auto; + border: 2px solid mat-color($foreground, base, 0.07); + + &-elements { + width: 100%; + } + } + + &-permission-start-message { + display: flex; + align-items: center; + justify-content: space-around; + height: 300px; + overflow: auto; + border: 2px solid mat-color($foreground, base, 0.07); + } + + &-permission-no-result{ + display: flex; + align-items: center; + justify-content: space-around; + width: 100%; + } + + &-permission-search { + &-input { + width: 100%; + + &-icon { + color: mat-color($foreground, disabled-button); + cursor: pointer; + + &:hover { + color: mat-color($foreground, base); + } + } + } + } + + &-list-option-item .mat-list-text { + display: flex; + flex-direction: row !important; + align-items: center; + } + + &-permission-action { + &[disabled] { + opacity: 0.6; + } + &:enabled { + color: mat-color($primary); + } + } + } +} diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.spec.ts b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.spec.ts new file mode 100644 index 0000000000..d6e869e282 --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.spec.ts @@ -0,0 +1,156 @@ +/*! + * @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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { AddPermissionPanelComponent } from './add-permission-panel.component'; +import { By } from '@angular/platform-browser'; +import { SearchService, setupTestBed, SearchConfigurationService } from '@alfresco/adf-core'; +import { Observable } from 'rxjs/Observable'; +import { fakeAuthorityListResult } from '../../../mock/add-permission.component.mock'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { DebugElement } from '@angular/core'; + +describe('AddPermissionPanelComponent', () => { + + let fixture: ComponentFixture; + let component: AddPermissionPanelComponent; + let element: HTMLElement; + let searchApiService: SearchService; + let debugElement: DebugElement; + + setupTestBed({ + imports: [ContentTestingModule], + providers: [SearchService, SearchConfigurationService] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AddPermissionPanelComponent); + debugElement = fixture.debugElement; + element = fixture.nativeElement; + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + function typeWordIntoSearchInput(word: string): void { + let inputDebugElement = debugElement.query(By.css('#searchInput')); + inputDebugElement.nativeElement.value = word; + inputDebugElement.nativeElement.focus(); + inputDebugElement.nativeElement.dispatchEvent(new Event('input')); + } + + it('should be able to render the component', () => { + expect(element.querySelector('#adf-add-permission-type-search')).not.toBeNull(); + expect(element.querySelector('#searchInput')).not.toBeNull(); + }); + + it('should show search results when user types something', async(() => { + searchApiService = fixture.componentRef.injector.get(SearchService); + spyOn(searchApiService, 'search').and.returnValue(Observable.of(fakeAuthorityListResult)); + expect(element.querySelector('#adf-add-permission-type-search')).not.toBeNull(); + expect(element.querySelector('#searchInput')).not.toBeNull(); + typeWordIntoSearchInput('a'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('#adf-add-permission-authority-results')).not.toBeNull(); + expect(element.querySelector('#result_option_0')).not.toBeNull(); + }); + })); + + it('should emit a select event with the selected items when an item is clicked', async(() => { + searchApiService = fixture.componentRef.injector.get(SearchService); + spyOn(searchApiService, 'search').and.returnValue(Observable.of(fakeAuthorityListResult)); + component.select.subscribe((items) => { + expect(items).not.toBeNull(); + expect(items[0].entry.id).toBeDefined(); + expect(items[0].entry.id).not.toBeNull(); + expect(items[0].entry.id).toBe(fakeAuthorityListResult.list.entries[0].entry.id); + }); + + typeWordIntoSearchInput('a'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const listElement: DebugElement = fixture.debugElement.query(By.css('#result_option_0')); + expect(listElement).not.toBeNull(); + listElement.triggerEventHandler('click', {}); + }); + })); + + it('should show the icon related on the nodeType', async(() => { + searchApiService = fixture.componentRef.injector.get(SearchService); + spyOn(searchApiService, 'search').and.returnValue(Observable.of(fakeAuthorityListResult)); + expect(element.querySelector('#adf-add-permission-type-search')).not.toBeNull(); + expect(element.querySelector('#searchInput')).not.toBeNull(); + typeWordIntoSearchInput('a'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('#adf-add-permission-authority-results')).not.toBeNull(); + expect(element.querySelector('#result_option_0 #add-person-icon')).toBeDefined(); + expect(element.querySelector('#result_option_0 #add-person-icon')).not.toBeNull(); + expect(element.querySelector('#result_option_2 #add-group-icon')).toBeDefined(); + expect(element.querySelector('#result_option_2 #add-group-icon')).not.toBeNull(); + }); + })); + + it('should clear the search when user delete the search input field', async(() => { + searchApiService = fixture.componentRef.injector.get(SearchService); + spyOn(searchApiService, 'search').and.returnValue(Observable.of(fakeAuthorityListResult)); + expect(element.querySelector('#adf-add-permission-type-search')).not.toBeNull(); + expect(element.querySelector('#searchInput')).not.toBeNull(); + typeWordIntoSearchInput('a'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('#adf-add-permission-authority-results')).not.toBeNull(); + expect(element.querySelector('#result_option_0')).not.toBeNull(); + const clearButton = fixture.debugElement.query(By.css('#adf-permission-clear-input')); + expect(clearButton).not.toBeNull(); + clearButton.triggerEventHandler('click', {}); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('#adf-add-permission-authority-results')).toBeNull(); + }); + }); + })); + + it('should remove element from selection when is clicked and already selected', async(() => { + searchApiService = fixture.componentRef.injector.get(SearchService); + spyOn(searchApiService, 'search').and.returnValue(Observable.of(fakeAuthorityListResult)); + component.selectedItems.push(fakeAuthorityListResult.list.entries[0]); + component.select.subscribe((items) => { + expect(items).not.toBeNull(); + expect(items[0]).toBeUndefined(); + expect(component.selectedItems.length).toBe(0); + }); + + typeWordIntoSearchInput('a'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const listElement: DebugElement = fixture.debugElement.query(By.css('#result_option_0')); + expect(listElement).not.toBeNull(); + listElement.triggerEventHandler('click', {}); + }); + })); + +}); diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.ts b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.ts new file mode 100644 index 0000000000..df35119d07 --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission-panel.component.ts @@ -0,0 +1,82 @@ +/*! + * @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, ViewEncapsulation, EventEmitter, Output, ViewChild } from '@angular/core'; +import { SearchPermissionConfigurationService } from './search-config-permission.service'; +import { SearchService, SearchConfigurationService } from '@alfresco/adf-core'; +import { SearchComponent } from '../../../search/components/search.component'; +import { FormControl } from '@angular/forms'; +import { debounceTime } from 'rxjs/operators'; +import { MinimalNodeEntity } from 'alfresco-js-api'; + +@Component({ + selector: 'adf-add-permission-panel', + templateUrl: './add-permission-panel.component.html', + styleUrls: ['./add-permission-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + providers: [ + { provide: SearchConfigurationService, useClass: SearchPermissionConfigurationService }, + SearchService + ] +}) +export class AddPermissionPanelComponent { + + @ViewChild('search') + search: SearchComponent; + + @Output() + select: EventEmitter = new EventEmitter(); + + searchInput: FormControl = new FormControl(); + searchedWord = ''; + debounceSearch: number = 200; + + selectedItems: MinimalNodeEntity[] = []; + + constructor() { + this.searchInput.valueChanges + .pipe( + debounceTime(this.debounceSearch) + ) + .subscribe((searchValue) => { + this.searchedWord = searchValue; + if (!searchValue) { + this.search.resetResults(); + } + }); + } + + elementClicked(item: MinimalNodeEntity) { + if (this.isAlreadySelected(item)) { + this.selectedItems.splice(this.selectedItems.indexOf(item), 1); + } else { + this.selectedItems.push(item); + } + this.select.emit(this.selectedItems); + } + + private isAlreadySelected(item: MinimalNodeEntity): boolean { + return this.selectedItems.indexOf(item) >= 0; + } + + clearSearch() { + this.searchedWord = ''; + this.selectedItems.splice(0, this.selectedItems.length); + this.search.resetResults(); + } + +} diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission.component.html b/lib/content-services/permission-manager/components/add-permission/add-permission.component.html new file mode 100644 index 0000000000..99d3792e9d --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission.component.html @@ -0,0 +1,14 @@ + + +
+ +
+ + diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission.component.scss b/lib/content-services/permission-manager/components/add-permission/add-permission.component.scss new file mode 100644 index 0000000000..c3324845cd --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission.component.scss @@ -0,0 +1,16 @@ +@mixin adf-add-permission-theme($theme) { + + $primary: map-get($theme, primary); + + .adf { + + &-permission-action { + &[disabled] { + opacity: 0.6; + } + &:enabled { + color: mat-color($primary); + } + } + } +} diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission.component.spec.ts b/lib/content-services/permission-manager/components/add-permission/add-permission.component.spec.ts new file mode 100644 index 0000000000..71c3f28321 --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission.component.spec.ts @@ -0,0 +1,102 @@ +/*! + * @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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { AddPermissionComponent } from './add-permission.component'; +import { AddPermissionPanelComponent } from './add-permission-panel.component'; +import { By } from '@angular/platform-browser'; +import { setupTestBed } from '@alfresco/adf-core'; +import { Observable } from 'rxjs/Observable'; +import { fakeAuthorityResults } from '../../../mock/add-permission.component.mock'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { NodePermissionService } from '../../services/node-permission.service'; + +describe('AddPermissionComponent', () => { + + let fixture: ComponentFixture; + let element: HTMLElement; + let nodePermissionService: NodePermissionService; + + setupTestBed({ + imports: [ + ContentTestingModule + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AddPermissionComponent); + element = fixture.nativeElement; + nodePermissionService = TestBed.get(NodePermissionService); + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it('should be able to render the component', () => { + expect(element.querySelector('#adf-add-permission-type-search')).not.toBeNull(); + expect(element.querySelector('#searchInput')).not.toBeNull(); + expect(element.querySelector('#adf-add-permission-actions')).not.toBeNull(); + const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); + expect(addButton.disabled).toBeTruthy(); + }); + + it('should enable the ADD button when a selection is sent', async(() => { + const addPermissionPanelComponent: AddPermissionPanelComponent = fixture.debugElement.query(By.directive(AddPermissionPanelComponent)).componentInstance; + addPermissionPanelComponent.select.emit(fakeAuthorityResults); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); + expect(addButton.disabled).toBeFalsy(); + }); + })); + + it('should emit a success event when the node is updated', async(() => { + fixture.componentInstance.selectedItems = fakeAuthorityResults; + spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(Observable.of({ id: 'fake-node-id'})); + + fixture.componentInstance.success.subscribe((node) => { + expect(node.id).toBe('fake-node-id'); + }); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); + addButton.click(); + }); + })); + + it('should emit an error event when the node update fail', async(() => { + fixture.componentInstance.selectedItems = fakeAuthorityResults; + spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(Observable.throw({ error: 'errored'})); + + fixture.componentInstance.error.subscribe((error) => { + expect(error.error).toBe('errored'); + }); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); + addButton.click(); + }); + })); + +}); diff --git a/lib/content-services/permission-manager/components/add-permission/add-permission.component.ts b/lib/content-services/permission-manager/components/add-permission/add-permission.component.ts new file mode 100644 index 0000000000..c6d4b1734d --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/add-permission.component.ts @@ -0,0 +1,61 @@ +/*! + * @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, ViewEncapsulation, EventEmitter, Input, Output } from '@angular/core'; +import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { NodePermissionService } from '../../services/node-permission.service'; + +@Component({ + selector: 'adf-add-permission', + templateUrl: './add-permission.component.html', + styleUrls: ['./add-permission.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AddPermissionComponent { + + @Input() + nodeId: string; + + @Output() + success: EventEmitter = new EventEmitter(); + + @Output() + error: EventEmitter = new EventEmitter(); + + selectedItems: MinimalNodeEntity[] = []; + currentNode: MinimalNodeEntryEntity; + currentNodeRoles: string[]; + + constructor(private nodePermissionService: NodePermissionService) { + } + + onSelect(selection: MinimalNodeEntity[]) { + this.selectedItems = selection; + } + + applySelection() { + this.nodePermissionService.updateNodePermissions(this.nodeId, this.selectedItems) + .subscribe( + (node) => { + this.success.emit(node); + }, + (error) => { + this.error.emit(error); + }); + } + +} diff --git a/lib/content-services/permission-manager/components/add-permission/search-config-permission.service.ts b/lib/content-services/permission-manager/components/add-permission/search-config-permission.service.ts new file mode 100644 index 0000000000..7b58ce3783 --- /dev/null +++ b/lib/content-services/permission-manager/components/add-permission/search-config-permission.service.ts @@ -0,0 +1,43 @@ +/*! + * @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 { QueryBody } from 'alfresco-js-api'; +import { SearchConfigurationInterface } from '@alfresco/adf-core'; + +export class SearchPermissionConfigurationService implements SearchConfigurationInterface { + + constructor() { + } + + public generateQueryBody(searchTerm: string, maxResults: number, skipCount: number): QueryBody { + const defaultQueryBody: QueryBody = { + query: { + query: searchTerm ? `authorityName:${searchTerm}* OR userName:${searchTerm}*` : searchTerm + }, + include: ['properties', 'aspectNames'], + paging: { + maxItems: maxResults, + skipCount: skipCount + }, + filterQueries: [ + /*tslint:disable-next-line */ + { query: "TYPE:'cm:authority'" }] + }; + + return defaultQueryBody; + } +} diff --git a/lib/content-services/permission-manager/components/inherited-button.directive.spec.ts b/lib/content-services/permission-manager/components/inherited-button.directive.spec.ts index 97202ec06b..4828b39779 100644 --- a/lib/content-services/permission-manager/components/inherited-button.directive.spec.ts +++ b/lib/content-services/permission-manager/components/inherited-button.directive.spec.ts @@ -17,7 +17,7 @@ import { SimpleInheritedPermissionTestComponent } from '../../mock/inherited-permission.component.mock'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { PermissionManagerModule } from '../../index'; +import { InheritPermissionDirective } from './inherited-button.directive'; import { NodesApiService, setupTestBed, CoreModule } from '@alfresco/adf-core'; import { Observable } from 'rxjs/Observable'; @@ -33,20 +33,20 @@ describe('InheritPermissionDirective', () => { setupTestBed({ imports: [ - CoreModule.forRoot(), - PermissionManagerModule + CoreModule.forRoot() ], declarations: [ + InheritPermissionDirective, SimpleInheritedPermissionTestComponent ] }); - beforeEach(() => { + beforeEach(async(() => { fixture = TestBed.createComponent(SimpleInheritedPermissionTestComponent); component = fixture.componentInstance; element = fixture.nativeElement; nodeService = TestBed.get(NodesApiService); - }); + })); it('should be able to render the simple component', async(() => { fixture.detectChanges(); diff --git a/lib/content-services/permission-manager/components/inherited-button.directive.ts b/lib/content-services/permission-manager/components/inherited-button.directive.ts index 01e5e21468..9bc1c3faa9 100644 --- a/lib/content-services/permission-manager/components/inherited-button.directive.ts +++ b/lib/content-services/permission-manager/components/inherited-button.directive.ts @@ -35,15 +35,18 @@ export class InheritPermissionDirective { @Output() updated: EventEmitter = new EventEmitter(); + @Output() + error: EventEmitter = new EventEmitter(); + constructor(private nodeService: NodesApiService) { } onInheritPermissionClicked() { this.nodeService.getNode(this.nodeId).subscribe((node: MinimalNodeEntryEntity) => { - const nodeBody = { permissions : {isInheritanceEnabled : !node.permissions.isInheritanceEnabled} }; - this.nodeService.updateNode(this.nodeId, nodeBody, {include: ['permissions'] }).subscribe((nodeUpdated: MinimalNodeEntryEntity) => { + const nodeBody = { permissions: { isInheritanceEnabled: !node.permissions.isInheritanceEnabled } }; + this.nodeService.updateNode(this.nodeId, nodeBody, { include: ['permissions'] }).subscribe((nodeUpdated: MinimalNodeEntryEntity) => { this.updated.emit(nodeUpdated); - }); + }, (error) => this.error.emit(error)); }); } diff --git a/lib/content-services/permission-manager/components/permission-list/permission-list.component.html b/lib/content-services/permission-manager/components/permission-list/permission-list.component.html index 2a1cce741f..4c9f58c330 100644 --- a/lib/content-services/permission-manager/components/permission-list/permission-list.component.html +++ b/lib/content-services/permission-manager/components/permission-list/permission-list.component.html @@ -48,6 +48,13 @@ + + + + + diff --git a/lib/content-services/permission-manager/components/permission-list/permission-list.component.ts b/lib/content-services/permission-manager/components/permission-list/permission-list.component.ts index e741ebce98..0a49a03841 100644 --- a/lib/content-services/permission-manager/components/permission-list/permission-list.component.ts +++ b/lib/content-services/permission-manager/components/permission-list/permission-list.component.ts @@ -35,6 +35,9 @@ export class PermissionListComponent implements OnInit { @Output() update: EventEmitter = new EventEmitter(); + @Output() + error: EventEmitter = new EventEmitter(); + permissionList: PermissionDisplayModel[]; settableRoles: any[]; actualNode: MinimalNodeEntryEntity; @@ -82,7 +85,7 @@ export class PermissionListComponent implements OnInit { saveNewRole(event: any, permissionRow: PermissionDisplayModel) { let updatedPermissionRole: PermissionElement = this.buildUpdatedPermission(event.value, permissionRow); - this.nodePermissionService.updatePermissionRoles(this.actualNode, updatedPermissionRole) + this.nodePermissionService.updatePermissionRole(this.actualNode, updatedPermissionRole) .subscribe((node: MinimalNodeEntryEntity) => { this.update.emit(updatedPermissionRole); }); @@ -96,4 +99,10 @@ export class PermissionListComponent implements OnInit { return permissionRole; } + removePermission(permissionRow: PermissionDisplayModel) { + this.nodePermissionService.removePermission(this.actualNode, permissionRow).subscribe((node) => { + this.update.emit(node); + }, (error) => this.error.emit(error)); + } + } diff --git a/lib/content-services/permission-manager/models/permission.model.ts b/lib/content-services/permission-manager/models/permission.model.ts index 7e9600e31c..3500496d58 100644 --- a/lib/content-services/permission-manager/models/permission.model.ts +++ b/lib/content-services/permission-manager/models/permission.model.ts @@ -31,7 +31,7 @@ export class PermissionDisplayModel implements PermissionElement { this.name = obj.name; this.accessStatus = obj.accessStatus; this.isInherited = obj.isInherited !== null && obj.isInherited !== undefined ? obj.isInherited : false; - this.icon = obj.icon ? obj.icon : 'lock_open'; + this.icon = obj.icon ? obj.icon : 'vpn_key'; } } } diff --git a/lib/content-services/permission-manager/permission-manager.module.ts b/lib/content-services/permission-manager/permission-manager.module.ts index ff918e5d2c..9a4385d5f7 100644 --- a/lib/content-services/permission-manager/permission-manager.module.ts +++ b/lib/content-services/permission-manager/permission-manager.module.ts @@ -21,10 +21,15 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; import { MaterialModule } from '../material.module'; import { PermissionListComponent } from './components/permission-list/permission-list.component'; +import { AddPermissionComponent } from './components/add-permission/add-permission.component'; +import { AddPermissionDialogComponent } from './components/add-permission/add-permission-dialog.component'; import { DataTableModule, DataColumnModule } from '@alfresco/adf-core'; import { InheritPermissionDirective } from './components/inherited-button.directive'; import { NodePermissionService } from './services/node-permission.service'; import { NoPermissionTemplateComponent } from './components/permission-list/no-permission.component'; +import { SearchModule } from '..'; +import { NodePermissionDialogService } from './services/node-permission-dialog.service'; +import { AddPermissionPanelComponent } from './components/add-permission/add-permission-panel.component'; @NgModule({ imports: [ @@ -34,20 +39,29 @@ import { NoPermissionTemplateComponent } from './components/permission-list/no-p MaterialModule, TranslateModule, DataTableModule, - DataColumnModule + DataColumnModule, + SearchModule ], declarations: [ PermissionListComponent, NoPermissionTemplateComponent, - InheritPermissionDirective + AddPermissionPanelComponent, + InheritPermissionDirective, + AddPermissionComponent, + AddPermissionDialogComponent ], providers: [ + NodePermissionDialogService, NodePermissionService ], + entryComponents: [ AddPermissionPanelComponent, AddPermissionComponent, AddPermissionDialogComponent ], exports: [ PermissionListComponent, NoPermissionTemplateComponent, - InheritPermissionDirective + AddPermissionPanelComponent, + InheritPermissionDirective, + AddPermissionComponent, + AddPermissionDialogComponent ] }) export class PermissionManagerModule {} diff --git a/lib/content-services/permission-manager/public-api.ts b/lib/content-services/permission-manager/public-api.ts index 3e76f09728..d49beb12bd 100644 --- a/lib/content-services/permission-manager/public-api.ts +++ b/lib/content-services/permission-manager/public-api.ts @@ -18,7 +18,12 @@ export * from './components/permission-list/permission-list.component'; export * from './components/permission-list/no-permission.component'; export * from './components/inherited-button.directive'; -export * from './services/node-permission.service'; export * from './models/permission.model'; +export * from './services/node-permission-dialog.service'; +export * from './services/node-permission.service'; +export * from './components/add-permission/add-permission-dialog-data.interface'; +export * from './components/add-permission/add-permission-panel.component'; +export * from './components/add-permission/add-permission.component'; +export * from './components/add-permission/add-permission-dialog.component'; export * from './permission-manager.module'; diff --git a/lib/content-services/permission-manager/services/node-permission-dialog.service.spec.ts b/lib/content-services/permission-manager/services/node-permission-dialog.service.spec.ts new file mode 100644 index 0000000000..afcd3b7c17 --- /dev/null +++ b/lib/content-services/permission-manager/services/node-permission-dialog.service.spec.ts @@ -0,0 +1,83 @@ +/*! + * @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 { TestBed, async } from '@angular/core/testing'; +import { AppConfigService, setupTestBed } from '@alfresco/adf-core'; +import { NodePermissionDialogService } from './node-permission-dialog.service'; +import { MatDialog } from '@angular/material'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { ContentTestingModule } from '../../testing/content.testing.module'; +import { NodePermissionService } from './node-permission.service'; + +describe('NodePermissionDialogService', () => { + + let service: NodePermissionDialogService; + let materialDialog: MatDialog; + let spyOnDialogOpen: jasmine.Spy; + let afterOpenObservable: Subject; + let nodePermissionService: NodePermissionService; + + setupTestBed({ + imports: [ContentTestingModule], + providers: [NodePermissionService] + }); + + beforeEach(() => { + let appConfig: AppConfigService = TestBed.get(AppConfigService); + appConfig.config.ecmHost = 'http://localhost:9876/ecm'; + service = TestBed.get(NodePermissionDialogService); + materialDialog = TestBed.get(MatDialog); + afterOpenObservable = new Subject(); + nodePermissionService = TestBed.get(NodePermissionService); + spyOnDialogOpen = spyOn(materialDialog, 'open').and.returnValue({ + afterOpen: () => afterOpenObservable, + afterClosed: () => Observable.of({}), + componentInstance: { + error: new Subject() + } + }); + }); + + it('should be able to create the service', () => { + expect(service).not.toBeNull(); + }); + + it('should be able to open the dialog showing node permissions', () => { + service.openAddPermissionDialog('fake-node-id', 'fake-title'); + expect(spyOnDialogOpen).toHaveBeenCalled(); + }); + + it('should throw an error if the update of the node fails', async(() => { + spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(Observable.throw({error : 'error'})); + spyOn(service, 'openAddPermissionDialog').and.returnValue(Observable.of({})); + service.updateNodePermissionByDialog('fake-node-id', 'fake-title').subscribe(() => { + Observable.throw('This call should fail'); + }, (error) => { + expect(error.error).toBe('error'); + }); + })); + + it('should return the updated node', async(() => { + spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(Observable.of({id : 'fake-node'})); + spyOn(service, 'openAddPermissionDialog').and.returnValue(Observable.of({})); + service.updateNodePermissionByDialog('fake-node-id', 'fake-title').subscribe((node) => { + expect(node.id).toBe('fake-node'); + }); + })); + +}); diff --git a/lib/content-services/permission-manager/services/node-permission-dialog.service.ts b/lib/content-services/permission-manager/services/node-permission-dialog.service.ts new file mode 100644 index 0000000000..b0c433021b --- /dev/null +++ b/lib/content-services/permission-manager/services/node-permission-dialog.service.ts @@ -0,0 +1,64 @@ +/*! + * @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 { MatDialog } from '@angular/material'; +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; +import { AddPermissionDialogComponent } from '../components/add-permission/add-permission-dialog.component'; +import { AddPermissionDialogData } from '../components/add-permission/add-permission-dialog-data.interface'; +import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { NodePermissionService } from './node-permission.service'; + +@Injectable() +export class NodePermissionDialogService { + + constructor(private dialog: MatDialog, + private nodePermissionService: NodePermissionService) { + } + + openAddPermissionDialog(nodeId: string, title?: string): Observable { + const confirm = new Subject(); + + confirm.subscribe({ + complete: this.close.bind(this) + }); + + const data: AddPermissionDialogData = { + nodeId: nodeId, + title: title, + confirm : confirm + }; + + this.openDialog(data, 'adf-add-permission-dialog', '630px'); + return confirm; + } + + private openDialog(data: any, currentPanelClass: string, chosenWidth: string) { + this.dialog.open(AddPermissionDialogComponent, { data, panelClass: currentPanelClass, width: chosenWidth }); + } + + close() { + this.dialog.closeAll(); + } + + updateNodePermissionByDialog(nodeId?: string, title?: string): Observable { + return this.openAddPermissionDialog(nodeId, title).switchMap((selection) => { + return this.nodePermissionService.updateNodePermissions(nodeId, selection); + }); + } +} diff --git a/lib/content-services/permission-manager/services/node-permission.service.spec.ts b/lib/content-services/permission-manager/services/node-permission.service.spec.ts index b45554d6b7..588f80426a 100644 --- a/lib/content-services/permission-manager/services/node-permission.service.spec.ts +++ b/lib/content-services/permission-manager/services/node-permission.service.spec.ts @@ -20,7 +20,10 @@ import { NodePermissionService } from './node-permission.service'; import { SearchService, NodesApiService, setupTestBed, CoreModule } from '@alfresco/adf-core'; import { MinimalNodeEntryEntity, PermissionElement } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; -import { fakeEmptyResponse, fakeNodeWithOnlyLocally, fakeSiteRoles, fakeSiteNodeResponse } from '../../mock/permission-list.component.mock'; +import { fakeEmptyResponse, fakeNodeWithOnlyLocally, fakeSiteRoles, fakeSiteNodeResponse, + fakeNodeToRemovePermission } from '../../mock/permission-list.component.mock'; +import { fakeAuthorityResults } from '../../mock/add-permission.component.mock'; +import { NodePermissionDialogService } from './node-permission-dialog.service'; describe('NodePermissionService', () => { @@ -33,6 +36,7 @@ describe('NodePermissionService', () => { CoreModule.forRoot() ], providers: [ + NodePermissionDialogService, NodePermissionService ] }); @@ -85,7 +89,7 @@ describe('NodePermissionService', () => { spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); - service.updatePermissionRoles(fakeNodeWithOnlyLocally, fakePermission).subscribe((node: MinimalNodeEntryEntity) => { + service.updatePermissionRole(fakeNodeWithOnlyLocally, fakePermission).subscribe((node: MinimalNodeEntryEntity) => { expect(node).not.toBeNull(); expect(node.id).toBe('fake-updated-node'); expect(node.permissions.locallySet.length).toBe(1); @@ -95,4 +99,53 @@ describe('NodePermissionService', () => { }); })); + it('should be able to remove a locally set permission', async(() => { + const fakePermission: PermissionElement = { + 'authorityId': 'FAKE_PERSON_1', + 'name': 'Contributor', + 'accessStatus' : 'ALLOWED' + }; + spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); + const fakeNodeCopy = Object.assign(fakeNodeToRemovePermission); + + service.removePermission(fakeNodeCopy, fakePermission).subscribe((node: MinimalNodeEntryEntity) => { + expect(node).not.toBeNull(); + expect(node.id).toBe('fake-updated-node'); + expect(node.permissions.locallySet.length).toBe(2); + expect(node.permissions.locallySet[0].authorityId).not.toBe(fakePermission.authorityId); + expect(node.permissions.locallySet[1].authorityId).not.toBe(fakePermission.authorityId); + }); + })); + + it('should be able to update locally set permissions on the node by node id', async(() => { + const fakeNodeCopy = Object.assign(fakeNodeWithOnlyLocally); + spyOn(nodeService, 'getNode').and.returnValue(Observable.of(fakeNodeCopy)); + spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); + spyOn(searchApiService, 'searchByQueryBody').and.returnValue(Observable.of(fakeSiteNodeResponse)); + spyOn(service, 'getGroupMemeberByGroupName').and.returnValue(Observable.of(fakeSiteRoles)); + + service.updateNodePermissions('fake-node-id', fakeAuthorityResults).subscribe((node: MinimalNodeEntryEntity) => { + expect(node).not.toBeNull(); + expect(node.id).toBe('fake-updated-node'); + expect(node.permissions.locallySet.length).toBe(4); + expect(node.permissions.locallySet[3].authorityId).not.toBe(fakeAuthorityResults[0].entry['cm:userName']); + expect(node.permissions.locallySet[2].authorityId).not.toBe(fakeAuthorityResults[1].entry['cm:userName']); + expect(node.permissions.locallySet[1].authorityId).not.toBe(fakeAuthorityResults[2].entry['cm:userName']); + }); + })); + + it('should be able to update locally permissions on the node', async(() => { + const fakeNodeCopy = Object.assign(fakeNodeWithOnlyLocally); + spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); + + service.updateLocallySetPermissions(fakeNodeCopy, fakeAuthorityResults, fakeSiteRoles).subscribe((node: MinimalNodeEntryEntity) => { + expect(node).not.toBeNull(); + expect(node.id).toBe('fake-updated-node'); + expect(node.permissions.locallySet.length).toBe(4); + expect(node.permissions.locallySet[3].authorityId).not.toBe(fakeAuthorityResults[0].entry['cm:userName']); + expect(node.permissions.locallySet[2].authorityId).not.toBe(fakeAuthorityResults[1].entry['cm:userName']); + expect(node.permissions.locallySet[1].authorityId).not.toBe(fakeAuthorityResults[2].entry['cm:userName']); + }); + })); + }); diff --git a/lib/content-services/permission-manager/services/node-permission.service.ts b/lib/content-services/permission-manager/services/node-permission.service.ts index 6c3bb34f39..63d74e73fa 100644 --- a/lib/content-services/permission-manager/services/node-permission.service.ts +++ b/lib/content-services/permission-manager/services/node-permission.service.ts @@ -18,8 +18,10 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { AlfrescoApiService, SearchService, NodesApiService } from '@alfresco/adf-core'; -import { QueryBody, MinimalNodeEntryEntity, PathElement, GroupMemberEntry, GroupsPaging, GroupMemberPaging, PermissionElement } from 'alfresco-js-api'; +import { QueryBody, MinimalNodeEntryEntity, MinimalNodeEntity, PathElement, GroupMemberEntry, GroupsPaging, GroupMemberPaging, PermissionElement } from 'alfresco-js-api'; import 'rxjs/add/operator/switchMap'; +import { of } from 'rxjs/observable/of'; +import { switchMap } from 'rxjs/operators'; @Injectable() export class NodePermissionService { @@ -42,7 +44,7 @@ export class NodePermissionService { }); } - updatePermissionRoles(node: MinimalNodeEntryEntity, updatedPermissionRole: PermissionElement): Observable { + updatePermissionRole(node: MinimalNodeEntryEntity, updatedPermissionRole: PermissionElement): Observable { let permissionBody = { permissions: { locallySet: []} }; const index = node.permissions.locallySet.map((permission) => permission.authorityId).indexOf(updatedPermissionRole.authorityId); permissionBody.permissions.locallySet = permissionBody.permissions.locallySet.concat(node.permissions.locallySet); @@ -54,6 +56,47 @@ export class NodePermissionService { return this.nodeService.updateNode(node.id, permissionBody); } + updateNodePermissions(nodeId: string, permissionList: MinimalNodeEntity[]): Observable { + return this.nodeService.getNode(nodeId).pipe( + switchMap(node => { + return this.getNodeRoles(node).pipe( + switchMap((nodeRoles) => of({node, nodeRoles}) ) + ); + }), + switchMap(({node, nodeRoles}) => this.updateLocallySetPermissions(node, permissionList, nodeRoles)) + ); + } + + updateLocallySetPermissions(node: MinimalNodeEntryEntity, nodes: MinimalNodeEntity[], nodeRole: string[]): Observable { + let permissionBody = { permissions: { locallySet: []} }; + const permissionList = this.transformNodeToPermissionElement(nodes, nodeRole[0]); + permissionBody.permissions.locallySet = node.permissions.locallySet ? node.permissions.locallySet.concat(permissionList) : permissionList; + return this.nodeService.updateNode(node.id, permissionBody); + } + + private transformNodeToPermissionElement(nodes: MinimalNodeEntity[], nodeRole: any): PermissionElement[] { + return nodes.map((node) => { + let newPermissionElement: PermissionElement = { + 'authorityId': node.entry.properties['cm:authorityName'] ? + node.entry.properties['cm:authorityName'] : + node.entry.properties['cm:userName'], + 'name': nodeRole, + 'accessStatus': 'ALLOWED' + }; + return newPermissionElement; + }); + } + + removePermission(node: MinimalNodeEntryEntity, permissionToRemove: PermissionElement): Observable { + let permissionBody = { permissions: { locallySet: [] } }; + const index = node.permissions.locallySet.map((permission) => permission.authorityId).indexOf(permissionToRemove.authorityId); + if (index !== -1) { + node.permissions.locallySet.splice(index, 1); + permissionBody.permissions.locallySet = node.permissions.locallySet; + return this.nodeService.updateNode(node.id, permissionBody); + } + } + private getGroupMembersBySiteName(siteName: string): Observable { const groupName = 'GROUP_site_' + siteName; return this.getGroupMemeberByGroupName(groupName) diff --git a/lib/content-services/search/components/search-trigger.directive.ts b/lib/content-services/search/components/search-trigger.directive.ts index 4c9b16eb68..f27f3cb892 100644 --- a/lib/content-services/search/components/search-trigger.directive.ts +++ b/lib/content-services/search/components/search-trigger.directive.ts @@ -201,7 +201,7 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { } private setValueAndClose(event: any | null): void { - if (this.isPanelOptionClicked(event)) { + if (this.isPanelOptionClicked(event) && !event.defaultPrevented) { this.setTriggerValue(event.target.textContent.trim()); this.onChange(event.target.textContent.trim()); this.element.nativeElement.focus(); diff --git a/lib/content-services/styles/_index.scss b/lib/content-services/styles/_index.scss index bf8be12a5d..547bbbbf41 100644 --- a/lib/content-services/styles/_index.scss +++ b/lib/content-services/styles/_index.scss @@ -15,6 +15,9 @@ @import '../content-node-selector/content-node-selector.component'; @import '../content-metadata/content-metadata.module'; @import '../permission-manager/components/permission-list/permission-list.component'; +@import '../permission-manager/components/add-permission/add-permission.component'; +@import '../permission-manager/components/add-permission/add-permission-dialog.component'; +@import '../permission-manager/components/add-permission/add-permission-panel.component'; @mixin adf-content-services-theme($theme) { @include adf-breadcrumb-theme($theme); @@ -30,4 +33,7 @@ @include adf-content-node-selector-dialog-theme($theme) ; @include adf-content-metadata-module-theme($theme); @include adf-permission-list-theme($theme); + @include adf-add-permission-theme($theme); + @include adf-add-permission-dialog-theme($theme); + @include adf-add-permission-panel-theme($theme); }