From 0635b7fd063040ad1e94af4413ddc273c7bd7fbc Mon Sep 17 00:00:00 2001 From: Dharan <14145706+dhrn@users.noreply.github.com> Date: Fri, 23 Apr 2021 13:20:28 +0530 Subject: [PATCH] [ACA-4361] permission layout modified (#6937) * * reusable data table column moved * [ACA-4361] permission layout modified * * build fixed * fix build * * import fixed * * null safety operation * * fixed comments * * fix lint * * wait for reload list * * remove sleep * * add sleep * * fix comments * * fix comments * * floating promises fix * * remove wait --- .../datatable/datatable.component.html | 3 + .../app/components/files/files.component.html | 2 +- .../demo-permissions.component.html | 19 +- .../demo-permissions.component.scss | 8 +- .../permissions/demo-permissions.component.ts | 43 +-- .../components/permission-list.component.md | 17 +- docs/core/components/data-column.component.md | 19 +- docs/docassets/images/adf-permission-list.png | Bin 42001 -> 33872 bytes .../components/permissions-component.e2e.ts | 67 ++-- .../components/site-permissions.e2e.ts | 19 +- .../pages/permissions.page.ts | 83 ++++- lib/cli/scripts/check-cs-env.ts | 2 +- lib/cli/scripts/docker-publish.ts | 2 - lib/cli/scripts/docker.ts | 4 +- .../src/lib/directives/public-api.ts | 1 + lib/content-services/src/lib/i18n/en.json | 37 ++- .../mock/permission-list.component.mock.ts | 83 ++++- .../add-permission-dialog-data.interface.ts | 8 +- .../add-permission-dialog.component.html | 120 ++++++- .../add-permission-dialog.component.scss | 16 +- .../add-permission-dialog.component.spec.ts | 182 +++++++++-- .../add-permission-dialog.component.ts | 76 ++++- .../add-permission-panel.component.html | 28 +- .../add-permission-panel.component.scss | 4 + .../add-permission-panel.component.ts | 17 +- .../add-permission.component.spec.ts | 38 +-- .../add-permission.component.ts | 33 +- .../search-config-permission.service.ts | 2 +- .../node-path-column.component.ts | 44 +++ .../permission-container.component.html | 84 +++++ .../permission-container.component.scss | 45 +++ .../permission-container.component.spec.ts | 99 ++++++ .../permission-container.component.ts | 78 +++++ .../permission-list.component.html | 186 ++++++----- .../permission-list.component.scss | 106 ++++-- .../permission-list.component.spec.ts | 308 ++++++++++-------- .../permission-list.component.ts | 78 ++--- .../permission-list.service.spec.ts | 149 +++++++++ .../permission-list.service.ts | 207 ++++++++++++ .../components/pop-over.directive.ts | 104 ++++++ .../user-icon-column.component.scss | 34 ++ .../user-icon-column.component.spec.ts | 118 +++++++ .../user-icon-column.component.ts | 69 ++++ .../user-name-column.component.scss | 24 ++ .../user-name-column.component.spec.ts | 136 ++++++++ .../user-name-column.component.ts | 74 +++++ .../user-role-column.component.ts | 69 ++++ .../permission-manager/models/member.model.ts | 85 +++++ .../role.model.ts} | 13 +- .../permission-manager.module.ts | 26 +- .../src/lib/permission-manager/public-api.ts | 9 +- .../node-permission-dialog.service.spec.ts | 16 +- .../node-permission-dialog.service.ts | 33 +- .../services/node-permission.service.spec.ts | 91 ++++-- .../services/node-permission.service.ts | 130 ++++++-- .../src/lib/styles/_index.scss | 6 +- .../data-column-header.component.ts | 39 +++ lib/core/data-column/data-column.component.ts | 3 + lib/core/data-column/data-column.module.ts | 7 +- lib/core/data-column/public-api.ts | 1 + .../datatable/datatable.component.html | 9 +- lib/core/datatable/data/data-column.model.ts | 1 + .../datatable/data/object-datacolumn.model.ts | 2 + .../dialog/add-permissions-dialog.page.ts | 65 ++-- lib/testing/src/lib/core/models/user.model.ts | 4 + .../core/pages/data-table-component.page.ts | 2 +- 66 files changed, 2803 insertions(+), 684 deletions(-) create mode 100644 lib/content-services/src/lib/permission-manager/components/node-path-column/node-path-column.component.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.html create mode 100644 lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.scss create mode 100644 lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.spec.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.spec.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/pop-over.directive.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.scss create mode 100644 lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.spec.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.scss create mode 100644 lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.spec.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.ts create mode 100644 lib/content-services/src/lib/permission-manager/components/user-role-column/user-role-column.component.ts create mode 100644 lib/content-services/src/lib/permission-manager/models/member.model.ts rename lib/content-services/src/lib/permission-manager/{components/permission-list/no-permission.component.ts => models/role.model.ts} (72%) create mode 100644 lib/core/data-column/data-column-header.component.ts diff --git a/demo-shell/src/app/components/datatable/datatable.component.html b/demo-shell/src/app/components/datatable/datatable.component.html index 11add398aa..130b544dc2 100644 --- a/demo-shell/src/app/components/datatable/datatable.component.html +++ b/demo-shell/src/app/components/datatable/datatable.component.html @@ -33,6 +33,9 @@ {{value | json }} + + STATUS + --> diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html index 4d7c7dd860..8e0d552894 100644 --- a/demo-shell/src/app/components/files/files.component.html +++ b/demo-shell/src/app/components/files/files.component.html @@ -410,7 +410,7 @@ (execute)="onManageMetadata($event)"> - - +
+
- -
- - -
- 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 9bbbfee35f..fd5f327d80 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,4 @@ -.app-inherit_permission_button { - padding-top: 20px; - display: flex; - justify-content: center; - padding-bottom: 20px; +.app-permission-section { + box-sizing: border-box; + height: 100%; } 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 c13ea55e32..476dec9a5f 100644 --- a/demo-shell/src/app/components/permissions/demo-permissions.component.ts +++ b/demo-shell/src/app/components/permissions/demo-permissions.component.ts @@ -15,11 +15,8 @@ * limitations under the License. */ -import { Component, Optional, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, Optional } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; -import { PermissionListComponent, NodePermissionDialogService } from '@alfresco/adf-content-services'; -import { MinimalNodeEntryEntity } from '@alfresco/js-api'; -import { NodesApiService, NotificationService } from '@alfresco/adf-core'; @Component({ selector: 'app-permissions', @@ -28,16 +25,9 @@ import { NodesApiService, NotificationService } from '@alfresco/adf-core'; }) export class DemoPermissionComponent implements OnInit { - @ViewChild('permissionList', { static: true }) - displayPermissionComponent: PermissionListComponent; - nodeId: string; - toggleStatus = false; - constructor(@Optional() private route: ActivatedRoute, - private nodeService: NodesApiService, - private nodePermissionDialogService: NodePermissionDialogService, - private notificationService: NotificationService) { + constructor(@Optional() private route: ActivatedRoute) { } ngOnInit() { @@ -48,34 +38,5 @@ export class DemoPermissionComponent implements OnInit { } }); } - this.nodeService - .getNode(this.nodeId, {include: ['permissions'] }) - .subscribe( (currentNode: MinimalNodeEntryEntity) => { - this.toggleStatus = currentNode.permissions?.isInheritanceEnabled ?? false; - }); } - - onUpdatedPermissions(node: MinimalNodeEntryEntity) { - this.toggleStatus = node.permissions?.isInheritanceEnabled ?? false; - this.displayPermissionComponent.reload(); - } - - reloadList() { - this.displayPermissionComponent.reload(); - } - - openAddPermissionDialog() { - this.nodePermissionDialogService - .updateNodePermissionByDialog(this.nodeId) - .subscribe( - () => this.displayPermissionComponent.reload(), - (error) => this.showErrorMessage(error) - ); - } - - showErrorMessage(error) { - const message = error.message ? error.message : error; - this.notificationService.openSnackMessage(message); - } - } diff --git a/docs/content-services/components/permission-list.component.md b/docs/content-services/components/permission-list.component.md index 434de777c7..ca7c48db39 100644 --- a/docs/content-services/components/permission-list.component.md +++ b/docs/content-services/components/permission-list.component.md @@ -2,7 +2,7 @@ Title: Permission List Component Added: v2.3.0 Status: Active -Last reviewed: 2018-11-20 +Last reviewed: 2021-4-17 --- # [Permission List Component](../../../lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.ts "Defined in permission-list.component.ts") @@ -14,22 +14,9 @@ Shows node permissions as a table. ## Basic Usage ```html - - + ``` -### [Transclusions](../../user-guide/transclusion.md) - -When the list is empty, the contents will simply say "No permissions" by default, -but you can also supply your own content: - -```html - - - Custom no permission template! - - -``` ## Class members diff --git a/docs/core/components/data-column.component.md b/docs/core/components/data-column.component.md index 4d27f19ffd..7168846807 100644 --- a/docs/core/components/data-column.component.md +++ b/docs/core/components/data-column.component.md @@ -120,7 +120,7 @@ To disable the tooltip your function can return `null` or an empty string. ### Column Template -You can provide custom column/cell templates that may contain other Angular components or HTML elements: +You can provide custom column/cell header and templates that may contain other Angular components or HTML elements: Every cell in the DataTable component is bound to the dynamic data context containing the following properties: @@ -185,6 +185,23 @@ In the Example below we will prepend `Hi!` to each file and folder name in the l +In the example below we will show capitalised custom name for a column: + + + +```html + + + + NAME + + + +``` + + + + In the Example below we will integrate the [adf-tag-node-list](../../content-services/components/tag-node-list.component.md) component within the document list. diff --git a/docs/docassets/images/adf-permission-list.png b/docs/docassets/images/adf-permission-list.png index 1cfe99c66f295e66e71d30033e31822de7f5937d..4e13e1b7050b437938b90ddce99446fd232cf49b 100644 GIT binary patch literal 33872 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�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 RI^5~#Os-fOROq|J{tsZNy5Rr- diff --git a/e2e/content-services/components/permissions-component.e2e.ts b/e2e/content-services/components/permissions-component.e2e.ts index b38f446ec7..1bdd5fd854 100644 --- a/e2e/content-services/components/permissions-component.e2e.ts +++ b/e2e/content-services/components/permissions-component.e2e.ts @@ -74,14 +74,13 @@ describe('Permissions Component', () => { }); const groupBody = { - id: StringUtil.generateRandomString(), + id: `GROUP_${StringUtil.generateRandomString()}`, displayName: StringUtil.generateRandomString() }; const fileOwnerUser = new UserModel(); const filePermissionUser = new UserModel(); - const duplicateUserPermissionMessage = 'One or more of the permissions you have set is already present : authority -> ' + filePermissionUser.username + ' / role -> Contributor'; const roleConsumerFolderModel = new FolderModel({ name: 'roleConsumer' + StringUtil.generateRandomString() }); const roleCoordinatorFolderModel = new FolderModel({ name: 'roleCoordinator' + StringUtil.generateRandomString() }); const roleCollaboratorFolderModel = new FolderModel({ name: 'roleCollaborator' + StringUtil.generateRandomString() }); @@ -135,7 +134,7 @@ describe('Permissions Component', () => { await contentList.rightClickOnRow(fileModel.name); await contentServicesPage.pressContextMenuActionNamed('Permission'); - await permissionsPage.addPermissionsDialog.checkPermissionContainerIsDisplayed(); + await permissionsPage.checkPermissionManagerDisplayed(); }); afterEach(async () => { @@ -163,8 +162,11 @@ describe('Permissions Component', () => { await permissionsPage.addPermissionsDialog.checkSearchUserInputIsDisplayed(); await permissionsPage.addPermissionsDialog.searchUserOrGroup(groupBody.id); await permissionsPage.addPermissionsDialog.clickUserOrGroup(groupBody.displayName); - - await permissionsPage.addPermissionsDialog.checkGroupIsAdded(groupBody.id); + await permissionsPage.addPermissionsDialog.selectRole(groupBody.displayName, 'Consumer'); + await expect(await permissionsPage.addPermissionsDialog.addButtonIsEnabled()).toBe(true, 'button should be enabled'); + await permissionsPage.addPermissionsDialog.clickAddButton(); + await expect(await notificationPage.getSnackBarMessage()).toEqual('Added 0 user(s) 1 group(s)'); + await permissionsPage.checkUserIsAdded(groupBody.id); }); it('[C277100] Should display EVERYONE group in the search result set', async () => { @@ -179,6 +181,15 @@ describe('Permissions Component', () => { await permissionsPage.addPermissionsDialog.checkResultListIsDisplayed(); await permissionsPage.addPermissionsDialog.checkUserOrGroupIsDisplayed('EVERYONE'); }); + + it('should be able to toggle the inherited permission', async () => { + await permissionsPage.checkPermissionListDisplayed(); + await expect(await permissionsPage.isInherited()).toBe(true, 'Inherited permission should be on'); + await permissionsPage.toggleInheritPermission(); + await expect(await notificationPage.getSnackBarMessage()).toContain('Disabled inherited permission', 'Disabled notification not shown'); + await notificationPage.waitForSnackBarToClose(); + await expect(await permissionsPage.isInherited()).toBe(false, 'Inherited permission should be off'); + }); }); describe('Changing and duplicate Permissions', () => { @@ -192,13 +203,19 @@ describe('Permissions Component', () => { await contentServicesPage.checkSelectedSiteIsDisplayed('My files'); await contentList.rightClickOnRow(fileModel.name); await contentServicesPage.pressContextMenuActionNamed('Permission'); + await permissionsPage.checkPermissionManagerDisplayed(); await permissionsPage.addPermissionButton.waitVisible(); await permissionsPage.addPermissionsDialog.clickAddPermissionButton(); await permissionsPage.addPermissionsDialog.checkAddPermissionDialogIsDisplayed(); await permissionsPage.addPermissionsDialog.checkSearchUserInputIsDisplayed(); await permissionsPage.addPermissionsDialog.searchUserOrGroup(filePermissionUser.firstName); await permissionsPage.addPermissionsDialog.clickUserOrGroup(filePermissionUser.firstName); - await permissionsPage.addPermissionsDialog.checkUserIsAdded(filePermissionUser.username); + await permissionsPage.addPermissionsDialog.selectRole(filePermissionUser.fullName, 'Contributor'); + await expect(await permissionsPage.addPermissionsDialog.addButtonIsEnabled()).toBe(true, 'button should be enabled'); + await permissionsPage.addPermissionsDialog.clickAddButton(); + await expect(await notificationPage.getSnackBarMessage()).toEqual('Added 1 user(s) 0 group(s)'); + await notificationPage.waitForSnackBarToClose(); + await permissionsPage.checkUserIsAdded(filePermissionUser.username); }); afterEach(async () => { @@ -207,8 +224,8 @@ describe('Permissions Component', () => { }); it('[C274691] Should be able to add a new User with permission to the file and also change locally set permissions', async () => { - await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(filePermissionUser.username)).toEqual('Contributor'); - await permissionsPage.addPermissionsDialog.clickRoleDropdownByUserOrGroupName(filePermissionUser.username); + await expect(await permissionsPage.getRoleCellValue(filePermissionUser.username)).toEqual('Contributor'); + await permissionsPage.clickRoleDropdownByUserOrGroupName(filePermissionUser.username); const roleDropdownOptions = permissionsPage.addPermissionsDialog.getRoleDropdownOptions(); await expect(await roleDropdownOptions.count()).toBe(5); @@ -220,16 +237,20 @@ describe('Permissions Component', () => { await BrowserActions.closeMenuAndDialogs(); await permissionsPage.changePermission(filePermissionUser.username, 'Collaborator'); - await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(filePermissionUser.username)).toEqual('Collaborator'); + await notificationPage.waitForSnackBarToClose(); + await expect(await permissionsPage.getRoleCellValue(filePermissionUser.username)).toEqual('Collaborator'); await permissionsPage.changePermission(filePermissionUser.username, 'Coordinator'); - await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(filePermissionUser.username)).toEqual('Coordinator'); + await notificationPage.waitForSnackBarToClose(); + await expect(await permissionsPage.getRoleCellValue(filePermissionUser.username)).toEqual('Coordinator'); await permissionsPage.changePermission(filePermissionUser.username, 'Editor'); - await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(filePermissionUser.username)).toEqual('Editor'); + await notificationPage.waitForSnackBarToClose(); + await expect(await permissionsPage.getRoleCellValue(filePermissionUser.username)).toEqual('Editor'); await permissionsPage.changePermission(filePermissionUser.username, 'Consumer'); - await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(filePermissionUser.username)).toEqual('Consumer'); + await notificationPage.waitForSnackBarToClose(); + await expect(await permissionsPage.getRoleCellValue(filePermissionUser.username)).toEqual('Consumer'); }); it('[C276980] Should not be able to duplicate User or Group to the locally set permissions', async () => { @@ -239,15 +260,15 @@ describe('Permissions Component', () => { await permissionsPage.addPermissionsDialog.checkSearchUserInputIsDisplayed(); await permissionsPage.addPermissionsDialog.searchUserOrGroup(filePermissionUser.firstName); await permissionsPage.addPermissionsDialog.clickUserOrGroup(filePermissionUser.firstName); - - await expect(await notificationPage.getSnackBarMessage()).toEqual(duplicateUserPermissionMessage); - await notificationHistoryPage.checkNotifyContains(duplicateUserPermissionMessage); + await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(filePermissionUser.fullName)).toEqual('Contributor'); + await expect(await permissionsPage.addPermissionsDialog.addButtonIsEnabled()).toBe(false, 'button should not be enabled'); }); it('[C276982] Should be able to remove User or Group from the locally set permissions', async () => { - await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(filePermissionUser.username)).toEqual('Contributor'); - await permissionsPage.addPermissionsDialog.clickDeletePermissionButton(); - await permissionsPage.addPermissionsDialog.checkUserIsDeleted(filePermissionUser.username); + await expect(await permissionsPage.getRoleCellValue(filePermissionUser.username)).toEqual('Contributor'); + await permissionsPage.clickDeletePermissionButton(filePermissionUser.username); + await permissionsPage.checkUserIsDeleted(filePermissionUser.username); + await expect(await notificationPage.getSnackBarMessage()).toEqual('User/Group deleted'); }); }); @@ -376,13 +397,9 @@ describe('Permissions Component', () => { await contentServicesPage.checkSelectedSiteIsDisplayed('My files'); await contentList.rightClickOnRow('RoleConsumer' + fileModel.name); await contentServicesPage.pressContextMenuActionNamed('Permission'); - await permissionsPage.addPermissionsDialog.checkPermissionInheritedButtonIsDisplayed(); - await permissionsPage.addPermissionButton.waitVisible(); - await permissionsPage.addPermissionsDialog.clickPermissionInheritedButton(); - await expect(await notificationPage.getSnackBarMessage()).toEqual('You are not allowed to change permissions'); - await permissionsPage.addPermissionsDialog.clickAddPermissionButton(); - await expect(await notificationPage.getSnackBarMessage()).toEqual('You are not allowed to change permissions'); - await notificationHistoryPage.checkNotifyContains('You are not allowed to change permissions'); + await permissionsPage.checkPermissionManagerDisplayed(); + await permissionsPage.errorElement.waitPresent(); + await expect(await permissionsPage.noPermissionContent()).toContain('This item no longer exists or you don\'t have permission to view it.'); }); }); }); diff --git a/e2e/content-services/components/site-permissions.e2e.ts b/e2e/content-services/components/site-permissions.e2e.ts index cb9f360c23..8ef969b5b9 100644 --- a/e2e/content-services/components/site-permissions.e2e.ts +++ b/e2e/content-services/components/site-permissions.e2e.ts @@ -36,6 +36,7 @@ import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; import { VersionManagePage } from '../../core/pages/version-manager.page'; import CONSTANTS = require('../../util/constants'); import { SitesApi } from '@alfresco/js-api'; +import { NotificationDemoPage } from '../../core/pages/notification.page'; describe('Permissions Component', () => { @@ -49,6 +50,7 @@ describe('Permissions Component', () => { const navigationBarPage = new NavigationBarPage(); const metadataViewPage = new MetadataViewPage(); const notificationHistoryPage = new NotificationHistoryPage(); + const notificationPage = new NotificationDemoPage(); const uploadDialog = new UploadDialogPage(); const versionManagePage = new VersionManagePage(); @@ -182,11 +184,9 @@ describe('Permissions Component', () => { await contentServicesPage.pressContextMenuActionNamed('Permission'); - await permissionsPage.addPermissionsDialog.checkPermissionInheritedButtonIsDisplayed(); + await permissionsPage.checkPermissionManagerDisplayed(); await permissionsPage.addPermissionButton.waitVisible(); - await browser.sleep(5000); - await permissionsPage.addPermissionsDialog.clickAddPermissionButton(); await permissionsPage.addPermissionsDialog.checkAddPermissionDialogIsDisplayed(); await permissionsPage.addPermissionsDialog.checkSearchUserInputIsDisplayed(); @@ -194,11 +194,18 @@ describe('Permissions Component', () => { await permissionsPage.addPermissionsDialog.searchUserOrGroup(consumerUser.username); await permissionsPage.addPermissionsDialog.clickUserOrGroup(consumerUser.firstName); - await permissionsPage.addPermissionsDialog.checkUserIsAdded(consumerUser.username); + await permissionsPage.addPermissionsDialog.selectRole(consumerUser.fullName, 'Site Collaborator'); + await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(consumerUser.fullName)).toEqual('Site Collaborator'); + await expect(await permissionsPage.addPermissionsDialog.addButtonIsEnabled()).toBe(true, 'Add button should be enabled'); + await permissionsPage.addPermissionsDialog.clickAddButton(); + await expect(await notificationPage.getSnackBarMessage()).toEqual('Added 1 user(s) 0 group(s)'); + await notificationPage.waitForSnackBarToClose(); - await expect(await permissionsPage.addPermissionsDialog.getRoleCellValue(consumerUser.username)).toEqual(CONSTANTS.CS_USER_ROLES_I18N.COLLABORATOR); + await permissionsPage.checkUserIsAdded(consumerUser.username); - await permissionsPage.addPermissionsDialog.clickRoleDropdownByUserOrGroupName(consumerUser.username); + await expect(await permissionsPage.getRoleCellValue(consumerUser.username)).toEqual(CONSTANTS.CS_USER_ROLES_I18N.COLLABORATOR); + + await permissionsPage.clickRoleDropdownByUserOrGroupName(consumerUser.username); const roleDropdownOptions = permissionsPage.addPermissionsDialog.getRoleDropdownOptions(); diff --git a/e2e/content-services/pages/permissions.page.ts b/e2e/content-services/pages/permissions.page.ts index 2b5bc40d4e..58b54ca83f 100644 --- a/e2e/content-services/pages/permissions.page.ts +++ b/e2e/content-services/pages/permissions.page.ts @@ -15,29 +15,84 @@ * limitations under the License. */ -import { DataTableComponentPage, AddPermissionsDialogPage, TestElement } from '@alfresco/adf-testing'; -import { browser, by, element } from 'protractor'; +import { + AddPermissionsDialogPage, + BrowserActions, + DataTableComponentPage, + DropdownPage, + TestElement +} from '@alfresco/adf-testing'; +import { browser, by } from 'protractor'; export class PermissionsPage { dataTableComponentPage = new DataTableComponentPage(); addPermissionsDialog = new AddPermissionsDialogPage(); + rootElement = 'adf-permission-manager-card'; + inheritedButton = '[data-automation-id="adf-inherit-toggle-button"]'; + errorElement = TestElement.byId('adf-permission-manager-error'); + localPermissionList = TestElement.byCss('[data-automation-id="adf-locally-set-permission"]'); addPermissionButton = TestElement.byCss("button[data-automation-id='adf-add-permission-button']"); - addPermissionDialog = element(by.css('adf-add-permission-dialog')); - searchUserInput = element(by.id('searchInput')); - searchResults = element(by.css('#adf-add-permission-authority-results #adf-search-results-content')); - addButton = element(by.id('add-permission-dialog-confirm-button')); - permissionInheritedButton = element.all(by.css('.app-inherit_permission_button button')).first(); - noPermissions = element(by.id('adf-no-permissions-template')); - deletePermissionButton = element(by.css(`button[data-automation-id='adf-delete-permission-button']`)); - permissionDisplayContainer = element(by.id('adf-permission-display-container')); - closeButton = TestElement.byCss('#add-permission-dialog-close-button'); async changePermission(name: string, role: string): Promise { - await this.addPermissionsDialog.clickRoleDropdownByUserOrGroupName(name); - await this.addPermissionsDialog.selectOption(role); + await browser.sleep(1000); + await this.clickRoleDropdownByUserOrGroupName(name); + await new DropdownPage().selectOption(role); + await this.dataTableComponentPage.checkRowByContentIsNotSelected(name); + } + + async checkUserIsAdded(id: string) { + const userOrGroupName = TestElement.byCss('div[data-automation-id="' + id + '"]'); + await userOrGroupName.waitPresent(); + } + + async getRoleCellValue(username: string): Promise { + const locator = this.dataTableComponentPage.getCellByRowContentAndColumn('Users and Groups', username, 'Role'); + return BrowserActions.getText(locator); + } + + async clickRoleDropdownByUserOrGroupName(name: string): Promise { + const row = this.dataTableComponentPage.getRow('Users and Groups', name); + await row.click(); + await BrowserActions.click(row.element(by.css('[id="adf-select-role-permission"] .mat-select-trigger'))); + await TestElement.byCss('.mat-select-panel').waitVisible(); + } + + async clickDeletePermissionButton(username: string): Promise { + const userOrGroupName = TestElement.byCss(`[data-automation-id="adf-delete-permission-button-${username}"]`); + await userOrGroupName.waitPresent(); + await userOrGroupName.click(); + } + + async checkUserIsDeleted(username: string): Promise { + const userOrGroupName = TestElement.byCss('div[data-automation-id="' + username + '"]'); + await userOrGroupName.waitNotPresent(); + } + + async noPermissionContent(): Promise { + const noPermission = TestElement.byCss('.adf-no-permission__template--text'); + return noPermission.getText(); + } + + async checkPermissionManagerDisplayed(): Promise { + await TestElement.byId(this.rootElement).waitVisible(); + } + + async checkPermissionListDisplayed(): Promise { await browser.sleep(500); - await this.dataTableComponentPage.checkRowIsNotSelected('Authority ID', name); + await this.localPermissionList.waitVisible(); + } + + async isInherited(): Promise { + const inheritButton = TestElement.byCss(this.inheritedButton); + await inheritButton.waitVisible(); + const appliedStyles = await inheritButton.getAttribute('class'); + return appliedStyles.indexOf('mat-checked') !== -1; + } + + async toggleInheritPermission(): Promise { + const inheritButton = TestElement.byCss(`${this.inheritedButton} label`); + await inheritButton.click(); } } diff --git a/lib/cli/scripts/check-cs-env.ts b/lib/cli/scripts/check-cs-env.ts index 7c53aec1a5..0741e66245 100755 --- a/lib/cli/scripts/check-cs-env.ts +++ b/lib/cli/scripts/check-cs-env.ts @@ -35,7 +35,7 @@ async function checkEnv() { await alfrescoJsApi.login(program.username, program.password); } catch (error) { - if (error?.error.code === 'ETIMEDOUT') { + if (error?.error?.code === 'ETIMEDOUT') { logger.error('The env is not reachable. Terminating'); process.exit(1); } diff --git a/lib/cli/scripts/docker-publish.ts b/lib/cli/scripts/docker-publish.ts index 6d1224f6ab..bbc2a0f089 100644 --- a/lib/cli/scripts/docker-publish.ts +++ b/lib/cli/scripts/docker-publish.ts @@ -17,10 +17,8 @@ * limitations under the License. */ - import * as docker from './docker'; export default function (args: any) { docker.default(args); } - diff --git a/lib/cli/scripts/docker.ts b/lib/cli/scripts/docker.ts index 25563747a4..46ecea748a 100644 --- a/lib/cli/scripts/docker.ts +++ b/lib/cli/scripts/docker.ts @@ -115,8 +115,8 @@ function main(args) { process.exit(1); } - if(args.pathProject === undefined) { - args.pathProject = resolve('./') + if (args.pathProject === undefined) { + args.pathProject = resolve('./'); } if (args.loginCheck === true) { diff --git a/lib/content-services/src/lib/directives/public-api.ts b/lib/content-services/src/lib/directives/public-api.ts index 476da38cbb..7164362a33 100644 --- a/lib/content-services/src/lib/directives/public-api.ts +++ b/lib/content-services/src/lib/directives/public-api.ts @@ -17,3 +17,4 @@ export * from './content-directive.module'; export * from './node-lock.directive'; +export * from './node-counter.directive'; diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index e8a90cfa47..c762987c78 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -385,7 +385,8 @@ "NO_PERMISSIONS": "No permissions" }, "ADD-PERMISSION": { - "SEARCH": "Search", + "SEARCH": "Search for users or groups to add", + "TITLE": "Add user or group to", "TYPE-MESSAGE": "Type something to start searching groups or people", "NO-RESULT": "No result found for this search", "ADD-ACTION": "ADD", @@ -393,7 +394,41 @@ "BASE-DIALOG-TITLE": "Search a group or people to add...", "EVERYONE": "EVERYONE" }, + "COLUMN": { + "NAME": "Users and Groups ({{ count }})", + "LOCATION": "Location", + "BULK-ROLE": "Set all role to" + }, + "LABELS": { + "ON": "On", + "OFF": "Off", + "DIRECT-PERMISSIONS": "Direct Applied Permission", + "INHERITED-PERMISSIONS": "Inherited Permission", + "INHERITED-SUBTITLE": "{{count}} users or groups are inheriting permission from a parent folder", + "SELECT-ROLE": "Select role" + }, + "MESSAGE": { + "EMPTY-PERMISSION": "No users/groups", + "EMPTY-SUBTITLE": "Add user/group to manage permission.", + "NO-MEMBERS": "Add groups or people to manage roles", + "PERMISSION-BULK-UPDATE-SUCCESS": "Updated {{user}} user(s) {{group}} group(s)", + "PERMISSION-UPDATE-SUCCESS": "User/Group updated", + "PERMISSION-UPDATE-FAIL": "Failed to update user/group", + "PERMISSION-BULK-DELETE-SUCCESS": "Deleted {{user}} user(s_ {{group}} group(s)", + "PERMISSION-DELETE-SUCCESS": "User/Group deleted", + "PERMISSION-DELETE-FAIL": "Failed to delete user/group", + "PERMISSION-ADD-SUCCESS": "Added {{user}} user(s) {{group}} group(s)", + "PERMISSION-ADD-FAIL": "Failed to add user/group", + "INHERIT-ENABLE-SUCCESS": "Enabled inherited permission", + "INHERIT-DISABLE-SUCCESS": "Disabled inherited permission", + "TOGGLE-PERMISSION-FAILED": "Failed to toggle inherit Permission" + }, + "ACTION": { + "DELETE": "Delete", + "ADD-PERMISSION": "Add User or Group" + }, "ERROR": { + "NOT-FOUND": "This item no longer exists or you don't have permission to view it.", "DUPLICATE-PERMISSION": "One or more of the permissions you have set is already present : {{list}}", "NOT-ALLOWED": "You are not allowed to change permissions" } diff --git a/lib/content-services/src/lib/mock/permission-list.component.mock.ts b/lib/content-services/src/lib/mock/permission-list.component.mock.ts index a80c6304a8..f583facd5c 100644 --- a/lib/content-services/src/lib/mock/permission-list.component.mock.ts +++ b/lib/content-services/src/lib/mock/permission-list.component.mock.ts @@ -100,7 +100,86 @@ export const fakeNodeWithPermissions: any = { } }; -export const fakeNodeInheritedOnly: any = { +export const fakeNodeInheritedOnly = { + 'allowableOperations': [ 'updatePermissions' ], + 'aspectNames': [ + 'cm:auditable', + 'cm:taggable', + 'cm:author', + 'cm:titled', + 'app:uifacets' + ], + 'createdAt': '2017-11-16T16:29:38.638+0000', + 'path': { + 'name': '/Company Home/Sites/testsite/documentLibrary', + 'isComplete': true, + 'elements': [ + { + 'id': '2be275a1-b00d-4e45-83d8-66af43ac2252', + 'name': 'Company Home' + }, + { + 'id': '1be10a97-6eb9-4b60-b6c6-1673900e9631', + 'name': 'Sites' + }, + { + 'id': 'e002c740-b8f9-482a-a554-8fff4e4c9dc0', + 'name': 'testsite' + }, + { + 'id': '71626fae-0c04-4d0c-a129-20fa4c178716', + 'name': 'documentLibrary' + } + ] + }, + 'isFolder': true, + 'isFile': false, + 'createdByUser': { + 'id': 'System', + 'displayName': 'System' + }, + 'modifiedAt': '2018-03-21T03:17:58.783+0000', + 'permissions': { + 'inherited': [ + { + 'authorityId': 'guest', + 'name': 'Read', + 'accessStatus': 'ALLOWED' + }, + { + 'authorityId': 'GROUP_EVERYONE', + 'name': 'Read', + 'accessStatus': 'ALLOWED' + } + ], + 'settable': [ + 'Contributor', + 'Collaborator', + 'Coordinator', + 'Editor', + 'Consumer' + ], + 'isInheritanceEnabled': true + }, + 'modifiedByUser': { + 'id': 'admin', + 'displayName': 'PedroH Hernandez' + }, + 'name': 'test', + 'id': 'f472543f-7218-403d-917b-7a5861257244', + 'nodeType': 'cm:folder', + 'properties': { + 'cm:title': 'test', + 'cm:author': 'yagud', + 'cm:taggable': [ + 'e8c8fbba-03ba-4fa6-86b1-f7ad7c296409' + ], + 'cm:description': 'sleepery', + 'app:icon': 'space-icon-default' + } +}; + +export const fakeReadOnlyNodeInherited = { 'aspectNames': [ 'cm:auditable', 'cm:taggable', @@ -231,7 +310,7 @@ export const fakeNodeWithOnlyLocally: any = { 'Editor', 'Consumer' ], - 'isInheritanceEnabled': true + 'isInheritanceEnabled': false }, 'modifiedByUser': { 'id': 'admin', diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog-data.interface.ts b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog-data.interface.ts index a0e7c15c02..4213b5469a 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog-data.interface.ts +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog-data.interface.ts @@ -15,11 +15,13 @@ * limitations under the License. */ -import { NodeEntry } from '@alfresco/js-api'; +import { Node, PermissionElement } from '@alfresco/js-api'; import { Subject } from 'rxjs'; +import { RoleModel } from '../../models/role.model'; export interface AddPermissionDialogData { title?: string; - nodeId: string; - confirm: Subject; + node: Node; + roles: RoleModel[]; + confirm: Subject; } diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.html b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.html index 2afaf69305..2eb11e6942 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.html +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.html @@ -1,15 +1,109 @@

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

- - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.scss b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.scss index 2c0dcdb7c9..ec9b1d1a9a 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.scss +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.scss @@ -25,7 +25,7 @@ .mat-dialog-content { margin: 0; - overflow: hidden; + overflow: auto; } .mat-dialog-actions { @@ -51,5 +51,19 @@ } } } + + .adf { + &-search-user-button { + width: 100%; + .mat-button-wrapper { + display: flex; + align-items: center; + } + } + + &-add-member-action { + padding: 0 15px; + } + } } } diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts index 417ecc8a11..3919af4275 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.spec.ts @@ -15,28 +15,46 @@ * limitations under the License. */ -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -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 { NodeEntry } from '@alfresco/js-api'; +import { Node, PermissionElement } from '@alfresco/js-api'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; import { Subject } from 'rxjs'; +import { AddPermissionPanelComponent } from './add-permission-panel.component'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { AddPermissionDialogComponent } from './add-permission-dialog.component'; import { AddPermissionDialogData } from './add-permission-dialog-data.interface'; import { fakeAuthorityResults } from '../../../mock/add-permission.component.mock'; -import { AddPermissionPanelComponent } from './add-permission-panel.component'; -import { TranslateModule } from '@ngx-translate/core'; describe('AddPermissionDialog', () => { let fixture: ComponentFixture; + let component: AddPermissionDialogComponent; let element: HTMLElement; const data: AddPermissionDialogData = { title: 'dead or alive you are coming with me', - nodeId: 'fake-node-id', - confirm: new Subject () + node: { + id: 'fake-node-id', + aspectNames: [], + isFile: true, + name: 'fake-node.pdf', + permissions: { + locallySet: [] + } + } as Node, + roles: [ + { + label: 'test', + role: 'Test' + }, + { + label: 'consumer', + role: 'Consumer' + } + ], + confirm: new Subject () }; const dialogRef = { close: jasmine.createSpy('close') @@ -50,13 +68,12 @@ describe('AddPermissionDialog', () => { providers: [ { provide: MatDialogRef, useValue: dialogRef }, { provide: MAT_DIALOG_DATA, useValue: data } - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + ] }); beforeEach(() => { - fixture = TestBed.createComponent(AddPermissionDialogComponent); + component = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); }); @@ -72,44 +89,139 @@ describe('AddPermissionDialog', () => { }); it('should close the dialog when close button is clicked', () => { - const closeButton: HTMLButtonElement = element.querySelector('#add-permission-dialog-close-button'); + const closeButton: HTMLButtonElement = element.querySelector('[data-automation-id="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'); + const confirmButton: HTMLButtonElement = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); expect(confirmButton.disabled).toBeTruthy(); }); - it('should enable the button when a selection is done', async(() => { + 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'); + let confirmButton: HTMLButtonElement = element.querySelector('[data-automation-id="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(); - }); - })); + await fixture.detectChanges(); + confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + expect(confirmButton.disabled).toBe(false); + }); - it('should stream the confirmed selection on the confirm subject', async(() => { + it('should update the role after selection', async (done) => { + spyOn(component, 'onMemberUpdate').and.callThrough(); + const addPermissionPanelComponent: AddPermissionPanelComponent = fixture.debugElement.query(By.directive(AddPermissionPanelComponent)).componentInstance; + let confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + expect(confirmButton.disabled).toBe(true); + addPermissionPanelComponent.select.emit([fakeAuthorityResults[0]]); + await fixture.detectChanges(); + expect(confirmButton.disabled).toBe(false); + confirmButton.click(); + await fixture.detectChanges(); + + const selectBox = fixture.debugElement.query(By.css(('[id="adf-select-role-permission"] .mat-select-trigger'))); + selectBox.triggerEventHandler('click', null); + fixture.detectChanges(); + + const options = fixture.debugElement.queryAll(By.css('mat-option')); + expect(options).not.toBeNull(); + expect(options.length).toBe(2); + options[0].triggerEventHandler('click', {}); + await fixture.detectChanges(); + expect(component.onMemberUpdate).toHaveBeenCalled(); + + data.confirm.subscribe((selection) => { + expect(selection.length).toBe(1); + done(); + }); + + confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + expect(confirmButton.disabled).toBe(false); + confirmButton.click(); + }); + + it('should update all the user role on header column update', async () => { + spyOn(component, 'onBulkUpdate').and.callThrough(); + const addPermissionPanelComponent: AddPermissionPanelComponent = fixture.debugElement.query(By.directive(AddPermissionPanelComponent)).componentInstance; + let confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + expect(confirmButton.disabled).toBe(true); + addPermissionPanelComponent.select.emit(fakeAuthorityResults); + await fixture.detectChanges(); + expect(confirmButton.disabled).toBe(false); + confirmButton.click(); + await fixture.detectChanges(); + + const selectBox = fixture.debugElement.query(By.css(('[id="adf-bulk-select-role-permission"] .mat-select-trigger'))); + selectBox.triggerEventHandler('click', null); + fixture.detectChanges(); + + const options = fixture.debugElement.queryAll(By.css('mat-option')); + expect(options).not.toBeNull(); + expect(options.length).toBe(2); + options[0].triggerEventHandler('click', {}); + await fixture.detectChanges(); + expect(component.onBulkUpdate).toHaveBeenCalled(); + + data.confirm.subscribe((selection) => { + expect(selection.length).toBe(3); + }); + + confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + expect(confirmButton.disabled).toBe(false); + confirmButton.click(); + }); + + it('should delete the user after selection', async () => { + spyOn(component, 'onMemberUpdate').and.callThrough(); + spyOn(component, 'onMemberDelete').and.callThrough(); + const addPermissionPanelComponent: AddPermissionPanelComponent = fixture.debugElement.query(By.directive(AddPermissionPanelComponent)).componentInstance; + let confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + expect(confirmButton.disabled).toBe(true); + addPermissionPanelComponent.select.emit(fakeAuthorityResults); + await fixture.detectChanges(); + expect(confirmButton.disabled).toBe(false); + confirmButton.click(); + await fixture.detectChanges(); + + const selectBox = fixture.debugElement.query(By.css(('[id="adf-select-role-permission"] .mat-select-trigger'))); + selectBox.triggerEventHandler('click', null); + fixture.detectChanges(); + + const options = fixture.debugElement.queryAll(By.css('mat-option')); + expect(options).not.toBeNull(); + expect(options.length).toBe(2); + options[0].triggerEventHandler('click', {}); + await fixture.detectChanges(); + expect(component.onMemberUpdate).toHaveBeenCalled(); + + confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + expect(confirmButton.disabled).toBe(true); + const deleteButton = element.querySelectorAll('[data-automation-id="adf-delete-permission-button"]') as any; + deleteButton[1].click(); + deleteButton[2].click(); + await fixture.detectChanges(); + + expect(confirmButton.disabled).toBe(false); + expect(component.onMemberDelete).toHaveBeenCalled(); + data.confirm.subscribe((selection) => { + expect(selection.length).toBe(1); + }); + + confirmButton.click(); + }); + + 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); + expect(fakeAuthorityResults[0].entry.id).toBe(selection[0].authorityId); }); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const confirmButton = element.querySelector('#add-permission-dialog-confirm-button'); - confirmButton.click(); - }); - })); + await fixture.detectChanges(); + const confirmButton = element.querySelector('[data-automation-id="add-permission-dialog-confirm-button"]'); + confirmButton.click(); + }); }); diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.ts b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.ts index 55c5db022d..4ec5a90667 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.ts +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-dialog.component.ts @@ -15,11 +15,11 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, Inject, ViewChild } from '@angular/core'; -import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { NodeEntry } from '@alfresco/js-api'; +import { Component, Inject, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { NodeEntry, PermissionElement } from '@alfresco/js-api'; import { AddPermissionDialogData } from './add-permission-dialog-data.interface'; -import { AddPermissionComponent } from '../add-permission/add-permission.component'; +import { MemberModel } from '../../models/member.model'; @Component({ selector: 'adf-add-permission-dialog', @@ -28,13 +28,15 @@ import { AddPermissionComponent } from '../add-permission/add-permission.compone encapsulation: ViewEncapsulation.None }) export class AddPermissionDialogComponent { + isSearchActive = true; + selectedMembers: MemberModel[] = []; - @ViewChild('addPermission') - addPermissionComponent: AddPermissionComponent; - + private existingMembers: PermissionElement[] = []; currentSelection: NodeEntry[] = []; - constructor(@Inject(MAT_DIALOG_DATA) public data: AddPermissionDialogData) { + constructor(@Inject(MAT_DIALOG_DATA) public data: AddPermissionDialogData, + private dialogRef: MatDialogRef) { + this.existingMembers = this.data.node.permissions.locallySet || []; } onSelect(items: NodeEntry[]) { @@ -42,7 +44,63 @@ export class AddPermissionDialogComponent { } onAddClicked() { - this.data.confirm.next(this.currentSelection); + const selection = this.selectedMembers.filter(member => !member.readonly).map(member => member.toPermissionElement()); + this.data.confirm.next(selection); this.data.confirm.complete(); } + + onSearchAddClicked() { + const newMembers = this.currentSelection.map(item => MemberModel.parseFromSearchResult(item)) + .filter(({id}) => !this.selectedMembers.find((member) => member.id === id)); + this.selectedMembers = this.selectedMembers.concat(newMembers); + + this.selectedMembers.forEach((member) => { + const existingMember = this.existingMembers.find(({authorityId}) => authorityId === member.id); + if (!!existingMember) { + member.role = existingMember.name; + member.accessStatus = existingMember.accessStatus; + member.readonly = true; // make role non editable + } + }); + this.disableSearch(); + } + + canCloseDialog() { + if (!!this.selectedMembers.length) { + this.disableSearch(); + } else { + this.dialogRef.close(); + } + } + + enableSearch() { + this.isSearchActive = true; + } + + disableSearch() { + this.isSearchActive = false; + } + + onBulkUpdate(role: string) { + this.selectedMembers.filter(member => !member.readonly) + .forEach(member => (member.role = role)); + } + + onMemberDelete({ id }: MemberModel) { + const index = this.selectedMembers.findIndex((member) => member.id === id); + this.selectedMembers.splice(index, 1); + if (this.selectedMembers.length === 0) { + this.enableSearch(); + this.currentSelection = []; + } + } + + onMemberUpdate(role: string, member: MemberModel) { + const _member = this.selectedMembers.find(({ id }) => id === member.id); + _member.role = role; + } + + isValid(): boolean { + return this.selectedMembers.filter(({readonly}) => !readonly).length && this.selectedMembers.every(({role}) => !!role); + } } diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.html b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.html index 6811c95d83..928c07ca31 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.html +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.html @@ -33,15 +33,12 @@ class="adf-permission-result-list" [class.adf-permission-result-list-search]="searchedWord.length === 0"> - + - - group_add - -

+ +

{{'PERMISSION_MANAGER.ADD-PERMISSION.EVERYONE' | translate}}

@@ -50,26 +47,19 @@ (click)="elementClicked(item)" class="adf-list-option-item" id="result_option_{{idx}}"> - - group_add - - - person_add - -

+ +

- {{item.entry?.properties['cm:authorityDisplayName']}} + {{item.entry.properties['cm:authorityDisplayName']}} - {{item.entry?.properties['cm:authorityName']}} + {{item.entry.properties['cm:authorityName']}} - {{item.entry?.properties['cm:owner']?.displayName}} + {{item.entry?.properties['cm:firstName'] ? item.entry?.properties['cm:firstName'] : '' }} + {{item.entry?.properties['cm:lastName'] ? item.entry?.properties['cm:lastName']: ''}}

diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.scss b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.scss index 8963bd8803..f154235ff9 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.scss +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.scss @@ -58,6 +58,10 @@ display: flex; flex-direction: row !important; align-items: center; + + .adf-result-name { + padding-left: 16px !important; + } } &-permission-action { diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.ts b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.ts index 17e12d439c..1dd95faa25 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.ts +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission-panel.component.ts @@ -15,13 +15,13 @@ * 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 { NodeEntry } from '@alfresco/js-api'; +import { Component, ViewEncapsulation, EventEmitter, Output, ViewChild } from '@angular/core'; import { FormControl } from '@angular/forms'; import { debounceTime } from 'rxjs/operators'; -import { NodeEntry } from '@alfresco/js-api'; +import { SearchPermissionConfigurationService } from './search-config-permission.service'; +import { SearchComponent } from '../../../search/components/search.component'; @Component({ selector: 'adf-add-permission-panel', @@ -48,7 +48,7 @@ export class AddPermissionPanelComponent { selectedItems: NodeEntry[] = []; - EVERYONE: NodeEntry = new NodeEntry({ entry: { properties: {'cm:authorityName': 'GROUP_EVERYONE'}}}); + EVERYONE: NodeEntry = new NodeEntry({ entry: { nodeType: 'cm:authorityContainer', properties: {'cm:authorityName': 'GROUP_EVERYONE'}}}); constructor() { this.searchInput.valueChanges @@ -72,6 +72,13 @@ export class AddPermissionPanelComponent { this.select.emit(this.selectedItems); } + selectAll(items: NodeEntry[]) { + if (items?.length > 0) { + this.selectedItems = items; + this.select.emit(this.selectedItems); + } + } + private isAlreadySelected(item: NodeEntry): boolean { return this.selectedItems.indexOf(item) >= 0; } diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.spec.ts b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.spec.ts index d3d027439f..4e3897c21c 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.spec.ts +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.spec.ts @@ -15,24 +15,23 @@ * limitations under the License. */ +import { setupTestBed } from '@alfresco/adf-core'; +import { Node } from '@alfresco/js-api'; 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, NodesApiService } from '@alfresco/adf-core'; +import { TranslateModule } from '@ngx-translate/core'; import { of, throwError } from 'rxjs'; import { fakeAuthorityResults } from '../../../mock/add-permission.component.mock'; -import { ContentTestingModule } from '../../../testing/content.testing.module'; import { NodePermissionService } from '../../services/node-permission.service'; -import { Node } from '@alfresco/js-api'; -import { TranslateModule } from '@ngx-translate/core'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; describe('AddPermissionComponent', () => { let fixture: ComponentFixture; let element: HTMLElement; let nodePermissionService: NodePermissionService; - let nodeApiService: NodesApiService; setupTestBed({ imports: [ @@ -42,11 +41,12 @@ describe('AddPermissionComponent', () => { }); beforeEach(() => { - nodeApiService = TestBed.inject(NodesApiService); - spyOn(nodeApiService, 'getNode').and.returnValue(of({ id: 'fake-node', allowableOperations: ['updatePermissions']})); + nodePermissionService = TestBed.inject(NodePermissionService); + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue( + of({ node: { id: 'fake-node', allowableOperations: ['updatePermissions']}, roles: [{ label: 'Test' , role: 'test'}] }) + ); fixture = TestBed.createComponent(AddPermissionComponent); element = fixture.nativeElement; - nodePermissionService = TestBed.inject(NodePermissionService); fixture.detectChanges(); }); @@ -84,7 +84,7 @@ describe('AddPermissionComponent', () => { }); })); - it('should emit a success event when the node is updated', (done) => { + it('should emit a success event when the node is updated', async (done) => { fixture.componentInstance.selectedItems = fakeAuthorityResults; spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(of({ id: 'fake-node-id'})); @@ -93,12 +93,9 @@ describe('AddPermissionComponent', () => { done(); }); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); - addButton.click(); - }); + await fixture.detectChanges(); + const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); + addButton.click(); }); it('should NOT emit a success event when the user does not have permission to update the node', () => { @@ -111,7 +108,7 @@ describe('AddPermissionComponent', () => { expect(spySuccess).not.toHaveBeenCalled(); }); - it('should emit an error event when the node update fail', (done) => { + it('should emit an error event when the node update fail', async (done) => { fixture.componentInstance.selectedItems = fakeAuthorityResults; spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(throwError({ error: 'err'})); @@ -120,11 +117,8 @@ describe('AddPermissionComponent', () => { done(); }); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); - addButton.click(); - }); + await fixture.detectChanges(); + const addButton: HTMLButtonElement = element.querySelector('#adf-add-permission-action-button'); + addButton.click(); }); }); diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.ts b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.ts index 0f2321b4f1..f36de1b5c0 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.ts +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/add-permission.component.ts @@ -15,10 +15,11 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, EventEmitter, Input, Output, OnInit } from '@angular/core'; -import { NodeEntry, Node } from '@alfresco/js-api'; +import { AllowableOperationsEnum, ContentService } from '@alfresco/adf-core'; +import { Node, NodeEntry, PermissionElement } from '@alfresco/js-api'; +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { NodePermissionService } from '../../services/node-permission.service'; -import { NodesApiService, ContentService, AllowableOperationsEnum } from '@alfresco/adf-core'; +import { RoleModel } from '../../models/role.model'; @Component({ selector: 'adf-add-permission', @@ -26,6 +27,9 @@ import { NodesApiService, ContentService, AllowableOperationsEnum } from '@alfre styleUrls: ['./add-permission.component.scss'], encapsulation: ViewEncapsulation.None }) +/* + * @deprecated in 4.4.0, use adf-add-permission-panel instead. + */ export class AddPermissionComponent implements OnInit { /** ID of the target node. */ @@ -42,14 +46,16 @@ export class AddPermissionComponent implements OnInit { selectedItems: NodeEntry[] = []; currentNode: Node; - currentNodeRoles: string[]; + currentNodeRoles: RoleModel[]; constructor(private nodePermissionService: NodePermissionService, - private nodeApiService: NodesApiService, private contentService: ContentService) { } ngOnInit(): void { - this.nodeApiService.getNode(this.nodeId).subscribe((node) => this.currentNode = node); + this.nodePermissionService.getNodeWithRoles(this.nodeId).subscribe(({node, roles }) => { + this.currentNode = node; + this.currentNodeRoles = roles; + }); } onSelect(selection: NodeEntry[]) { @@ -63,9 +69,9 @@ export class AddPermissionComponent implements OnInit { applySelection() { if (this.contentService.hasAllowableOperations(this.currentNode, AllowableOperationsEnum.UPDATEPERMISSIONS)) { - this.nodePermissionService.updateNodePermissions(this.nodeId, this.selectedItems) - .subscribe( - (node) => { + const permissions = this.transformNodeToPermissionElement(this.selectedItems, this.currentNodeRoles[0].role); + this.nodePermissionService.updateNodePermissions(this.nodeId, permissions) + .subscribe((node) => { this.success.emit(node); }, (error) => { @@ -74,4 +80,13 @@ export class AddPermissionComponent implements OnInit { } } + private transformNodeToPermissionElement(nodes: NodeEntry[], role: string): PermissionElement[] { + return nodes.map((node) => { + return { + 'authorityId': node.entry.properties['cm:authorityName'] ?? node.entry.properties['cm:userName'], + 'name': role, + 'accessStatus': 'ALLOWED' + }; + }); + } } diff --git a/lib/content-services/src/lib/permission-manager/components/add-permission/search-config-permission.service.ts b/lib/content-services/src/lib/permission-manager/components/add-permission/search-config-permission.service.ts index e3b75f69d2..de07f9c4f0 100644 --- a/lib/content-services/src/lib/permission-manager/components/add-permission/search-config-permission.service.ts +++ b/lib/content-services/src/lib/permission-manager/components/add-permission/search-config-permission.service.ts @@ -57,7 +57,7 @@ export class SearchPermissionConfigurationService implements SearchConfiguration query = this.queryProvider.query.replace( new RegExp(/\${([^}]+)}/g), searchTerm); } else { - query = `authorityName:*${searchTerm}* OR userName:*${searchTerm}*`; + query = `(email:*${searchTerm}* OR firstName:*${searchTerm}* OR lastName:*${searchTerm}* OR displayName:*${searchTerm}* OR authorityName:*${searchTerm}* OR authorityDisplayName:*${searchTerm}*) AND ANAME:(\"0/APP.DEFAULT\")`; } return query; } diff --git a/lib/content-services/src/lib/permission-manager/components/node-path-column/node-path-column.component.ts b/lib/content-services/src/lib/permission-manager/components/node-path-column/node-path-column.component.ts new file mode 100644 index 0000000000..9a53ad40dd --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/node-path-column/node-path-column.component.ts @@ -0,0 +1,44 @@ +/*! + * @license + * Copyright 2019 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 { Node } from '@alfresco/js-api'; +import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Component({ + selector: 'adf-node-path-column', + template: ` + + {{ displayText$ | async }} + + `, + host: { class: 'adf-node-path-column adf-datatable-content-cell' } +}) +export class NodePathColumnComponent implements OnInit { + @Input() + node: Node; + + displayText$ = new BehaviorSubject(''); + + ngOnInit() { + this.updateValue(); + } + + protected updateValue() { + this.displayText$.next(this.node.path.name); + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.html b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.html new file mode 100644 index 0000000000..82f9f651fb --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.scss b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.scss new file mode 100644 index 0000000000..30dd65d9c5 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.scss @@ -0,0 +1,45 @@ +@mixin adf-permission-container-theme($theme) { + $adf-permission-list-width: 100% !default; + + .adf { + &-permission-label { + max-width: 130px; + min-width: 100px; + margin-left: 50px; + } + + &-delete-permission { + max-width: 50px; + } + + &-authorityId-label { + min-width: 100px; + } + + &-key-icon { + max-width: 50px; + } + + &-ellipsis-cell { + position: sticky; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-display-permission-container { + display: flex; + justify-content: space-around; + flex: 1; + } + + &-datatable-permission { + display: flex; + min-width: 450px; + width: $adf-permission-list-width; + + &.adf-datatable { + overflow: hidden; + } + } + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.spec.ts b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.spec.ts new file mode 100644 index 0000000000..b55bc54741 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.spec.ts @@ -0,0 +1,99 @@ +/*! + * @license + * Copyright 2019 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 { setupTestBed } from '@alfresco/adf-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; +import { PermissionContainerComponent } from './permission-container.component'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; + +describe('PermissionContainerComponent', () => { + + let fixture: ComponentFixture; + let component: PermissionContainerComponent; + let element: HTMLElement; + + setupTestBed({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PermissionContainerComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.permissions = [ + { + authorityId: 'GROUP_EVERYONE', + accessStatus: 'ALLOWED', + isInherited: true, + name: 'consumer', + icon: null + } + ]; + + component.roles = [ + { + label: 'test', + role: 'Test' + }, + { + label: 'consumr', + role: 'Consumer' + } + ]; + + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it('Should render the layout with details', () => { + expect(element.querySelectorAll('.adf-datatable-permission .adf-datatable-row').length).toBe(2); + expect(element.querySelector('adf-user-name-column').textContent).toContain('GROUP_EVERYONE'); + expect(element.querySelector('#adf-select-role-permission').textContent).toContain('consumer'); + }); + + it('should emit update event on role change', () => { + spyOn(component.update, 'emit'); + + const selectBox = fixture.debugElement.query(By.css(('[id="adf-select-role-permission"] .mat-select-trigger'))); + selectBox.triggerEventHandler('click', null); + fixture.detectChanges(); + + const options = fixture.debugElement.queryAll(By.css('mat-option')); + expect(options).not.toBeNull(); + expect(options.length).toBe(2); + options[0].triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(component.update.emit).toHaveBeenCalledWith({ role: 'Test', permission: component.permissions[0] }); + }); + + it('should delete update event on row delete', () => { + spyOn(component.delete, 'emit'); + const deleteButton: HTMLButtonElement = element.querySelector('[data-automation-id="adf-delete-permission-button-GROUP_EVERYONE"]'); + deleteButton.click(); + fixture.detectChanges(); + expect(component.delete.emit).toHaveBeenCalledWith(component.permissions[0]); + }); +}); diff --git a/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.ts b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.ts new file mode 100644 index 0000000000..e635e6a1df --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/permission-container/permission-container.component.ts @@ -0,0 +1,78 @@ +/*! + * @license + * Copyright 2019 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, EventEmitter, Input, OnChanges, Output } from '@angular/core'; +import { Node, PermissionElement } from '@alfresco/js-api'; +import { PermissionDisplayModel } from '../../models/permission.model'; +import { RoleModel } from '../../models/role.model'; + +@Component({ + selector: 'adf-permission-container', + templateUrl: './permission-container.component.html', + styleUrls: ['./permission-container.component.scss'] +}) +export class PermissionContainerComponent implements OnChanges { + + @Input() + node: Node; + + @Input() + permissions: PermissionDisplayModel[] = []; + + @Input() + roles!: RoleModel[]; + + @Input() + isReadOnly = false; + + @Input() + showLocation = false; + + /** Emitted when the permission is updated. */ + @Output() + update = new EventEmitter<{role: string, permission: PermissionDisplayModel}>(); + + @Output() + updateAll = new EventEmitter(); + + /** Emitted when the permission is updated. */ + @Output() + delete = new EventEmitter(); + + /** Emitted when an error occurs. */ + @Output() + error = new EventEmitter(); + + bulkSelectionRole: string; + + ngOnChanges(): void { + this.bulkSelectionRole = ''; + } + + updateRole(role: string, permission: PermissionDisplayModel) { + this.update.emit({ role, permission }); + } + + bulkRoleUpdate(role: string) { + this.updateAll.emit(role); + } + + removePermission(event: MouseEvent, permissionRow: PermissionDisplayModel) { + event.stopPropagation(); + this.delete.emit(permissionRow); + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.html b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.html index fc34265769..7277452c93 100644 --- a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.html +++ b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.html @@ -1,84 +1,106 @@ -
- -
-
- -
- -

- {{ 'PERMISSION_MANAGER.PERMISSION_DISPLAY.NO_PERMISSIONS' | translate }} -

+ +
+ +
- - - - - - - - - - - {{ role | adfLocalizedRole }} - - - - {{ entry.data.getValue(entry.row, entry.col) | adfLocalizedRole }} - - - - - - - {{'PERMISSION_MANAGER.PERMISSION_DISPLAY.INHERITED' | translate}} - - - - - {{'PERMISSION_MANAGER.PERMISSION_DISPLAY.LOCALLY_SET' | translate}} - - - - - - - - - - - - -
+ +
+ error +

{{ 'PERMISSION_MANAGER.ERROR.NOT-FOUND'| translate }}

+
+
+ + +
+ +
+ +

+ {{'PERMISSION_MANAGER.LABELS.INHERITED-PERMISSIONS' | translate }} + + {{ (model.node.permissions.isInheritanceEnabled ? "PERMISSION_MANAGER.LABELS.ON" : "PERMISSION_MANAGER.LABELS.OFF") | translate }} +

+ + + +
+ + + {{'PERMISSION_MANAGER.LABELS.INHERITED-SUBTITLE' | translate: { count: model.inheritedPermissions.length } }} + +
+ + +
+ + +
+ + +
+
+ + +
+

{{'PERMISSION_MANAGER.LABELS.DIRECT-PERMISSIONS' | translate }}

+ +
+ + + + + + +
+ + + + +
+
+ diff --git a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.scss b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.scss index 2947029932..d6049f3827 100644 --- a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.scss +++ b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.scss @@ -1,50 +1,92 @@ @mixin adf-permission-list-theme($theme) { - $adf-permission-list-width: 70% !default; + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); .adf { - &-permission-label { - max-width: 130px; - min-width: 100px; - margin-left: 50px; + &-permission-card { + height: 100%; + box-sizing: border-box; + display: flex !important; + flex-direction: column; + overflow: hidden; } - &-delete-permission { - max-width: 50px; + &-permission-loader { + margin-left: 45%; + overflow: hidden; } - &-authorityId-label { - min-width: 100px; - } - - &-key-icon { - max-width: 50px; - } - - &-ellipsis-cell { - position: sticky; - text-overflow: ellipsis; - white-space: nowrap; - } - - &-display-permission-container { + &-permission-container { display: flex; - justify-content: space-around; - flex: 1; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + border: 1px solid mat-color($foreground, divider); } - &-datatable-permission { + &-inherit-container { display: flex; - min-width: 450px; - width: $adf-permission-list-width; + flex-direction: row; + align-items: center; + + &-header { + margin-bottom: 10px; + margin-top: 10px; + } } - &-locallyset-label { - padding: 4px; + &-inherit-toggle { + padding-left: 30px; } - &-inherited-label { - width: 92.13px; - justify-content: center; + &-inherit-subtitle { + padding-bottom: 5px; + } + + &-permission-content-header { + display: flex; + flex-direction: row; + align-items: center; + padding: 5px 15px; + } + + &-permission-role-column-header { + position: relative !important; + height: 40px; + .mat-form-field-infix { + border: none; + } + } + + &-permission-header { + @include flex-column; + } + + &-permission-list { + display: flex; + height: calc(100% - 63px); + } + } + + + [aria-sort='Ascending'] adf-user-role-column, + [aria-sort='Descending'] adf-user-role-column { + padding-left: 10px; + padding-right: 10px; + } + + + .adf-permission-pop-over { + padding-right: 15px; + width: 100%; + + .adf-pop-over-card { + width: 100%; + @include mat-elevation(16, mat-color($foreground, divider), 0.8); } } } diff --git a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.spec.ts b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.spec.ts index 3d002e3706..7e7f3dbdea 100644 --- a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.spec.ts +++ b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.spec.ts @@ -15,21 +15,24 @@ * limitations under the License. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { PermissionListComponent } from './permission-list.component'; -import { By } from '@angular/platform-browser'; import { NodesApiService, SearchService, setupTestBed } from '@alfresco/adf-core'; -import { of } from 'rxjs'; -import { NodePermissionService } from '../../services/node-permission.service'; -import { fakeNodeWithPermissions, - fakeNodeInheritedOnly, - fakeNodeWithOnlyLocally, - fakeSiteNodeResponse, - fakeSiteRoles, - fakeNodeWithoutPermissions, - fakeEmptyResponse } from '../../../mock/permission-list.component.mock'; -import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; +import { of, throwError } from 'rxjs'; +import { PermissionListComponent } from './permission-list.component'; +import { NodePermissionService } from '../../services/node-permission.service'; +import { + fakeEmptyResponse, + fakeNodeInheritedOnly, + fakeNodeWithOnlyLocally, + fakeNodeWithoutPermissions, + fakeNodeWithPermissions, + fakeReadOnlyNodeInherited, + fakeSiteNodeResponse, + fakeSiteRoles +} from '../../../mock/permission-list.component.mock'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; describe('PermissionListComponent', () => { @@ -39,6 +42,9 @@ describe('PermissionListComponent', () => { let nodeService: NodesApiService; let nodePermissionService: NodePermissionService; let searchApiService: SearchService; + let getNodeSpy: jasmine.Spy; + let searchQuerySpy: jasmine.Spy; + const fakeLocalPermission = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); setupTestBed({ imports: [ @@ -54,142 +60,188 @@ describe('PermissionListComponent', () => { nodeService = TestBed.inject(NodesApiService); nodePermissionService = TestBed.inject(NodePermissionService); searchApiService = TestBed.inject(SearchService); + + spyOn(nodePermissionService, 'getGroupMemberByGroupName').and.returnValue(of(fakeSiteRoles)); + getNodeSpy = spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithoutPermissions)); + searchQuerySpy = spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeEmptyResponse)); + component.nodeId = 'fake-node-id'; + fixture.detectChanges(); }); afterEach(() => { fixture.destroy(); }); - it('should be able to render the component', () => { - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithOnlyLocally)); - spyOn(nodePermissionService, 'getNodeRoles').and.returnValue(of([])); - fixture.detectChanges(); - expect(element.querySelector('#adf-permission-display-container')).not.toBeNull(); + it('should render default layout', async () => { + component.nodeId = 'fake-node-id'; + getNodeSpy.and.returnValue(of(fakeNodeWithoutPermissions)); + component.ngOnInit(); + await fixture.detectChanges(); + expect(element.querySelector('.adf-permission-container')).not.toBeNull(); + expect(element.querySelector('[data-automation-id="adf-locally-set-permission"]')).not.toBeNull(); }); - it('should render default empty template when no permissions', () => { + it('should render error template', async () => { component.nodeId = 'fake-node-id'; - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithoutPermissions)); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeEmptyResponse)); - fixture.detectChanges(); + getNodeSpy.and.returnValue(throwError(null)); + component.ngOnInit(); + await fixture.detectChanges(); - expect(element.querySelector('#adf-no-permissions-template')).not.toBeNull(); - expect(element.querySelector('#adf-permission-display-container .adf-datatable-permission')).toBeNull(); + expect(element.querySelector('.adf-no-permission__template')).not.toBeNull(); + expect(element.querySelector('.adf-no-permission__template p').textContent).toContain('PERMISSION_MANAGER.ERROR.NOT-FOUND'); }); it('should show the node permissions', () => { component.nodeId = 'fake-node-id'; - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithPermissions)); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeEmptyResponse)); + getNodeSpy.and.returnValue(of(fakeNodeWithPermissions)); + component.ngOnInit(); fixture.detectChanges(); - expect(element.querySelector('#adf-permission-display-container')).not.toBeNull(); - expect(element.querySelectorAll('.adf-datatable-row').length).toBe(4); + + expect(element.querySelectorAll('[data-automation-id="adf-locally-set-permission"] .adf-datatable-row').length).toBe(2); + + const showButton: HTMLButtonElement = element.querySelector('[data-automation-id="permission-info-button"]'); + showButton.click(); + fixture.detectChanges(); + + expect(document.querySelectorAll('[data-automation-id="adf-inherited-permission"] .adf-datatable-row').length).toBe(3); }); - it('should show inherited label for inherited permissions', () => { - component.nodeId = 'fake-node-id'; - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeInheritedOnly)); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeEmptyResponse)); - fixture.detectChanges(); - expect(element.querySelector('#adf-permission-display-container')).not.toBeNull(); - expect(element.querySelector('#adf-permission-inherited-label')).toBeDefined(); - expect(element.querySelector('#adf-permission-inherited-label')).not.toBeNull(); - }); + describe('Inherited Permission', () => { + it('should show inherited details', async() => { + getNodeSpy.and.returnValue(of(fakeNodeInheritedOnly)); + component.ngOnInit(); + await fixture.detectChanges(); - describe('when it is a locally set permission', () => { - - it('should show locally set label for locally set permissions', () => { - component.nodeId = 'fake-node-id'; - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithOnlyLocally)); - spyOn(nodePermissionService, 'getGroupMemberByGroupName').and.returnValue(of(fakeSiteRoles)); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeSiteNodeResponse)); - fixture.detectChanges(); - expect(element.querySelector('#adf-permission-display-container')).not.toBeNull(); - expect(element.querySelector('#adf-permission-locallyset-label')).toBeDefined(); - expect(element.querySelector('#adf-permission-locallyset-label')).not.toBeNull(); + expect(element.querySelector('.adf-inherit-container .mat-checked')).toBeDefined(); + expect(element.querySelector('.adf-inherit-container h3').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-PERMISSIONS PERMISSION_MANAGER.LABELS.ON'); + expect(element.querySelector('span[title="total"]').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-SUBTITLE'); }); - it('should show a dropdown with the possible roles', async(() => { - component.nodeId = 'fake-node-id'; - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithOnlyLocally)); - spyOn(nodePermissionService, 'getGroupMemberByGroupName').and.returnValue(of(fakeSiteRoles)); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeSiteNodeResponse)); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(element.querySelector('#adf-select-role-permission')).toBeDefined(); - expect(element.querySelector('#adf-select-role-permission')).not.toBeNull(); - const selectBox = fixture.debugElement.query(By.css(('#adf-select-role-permission .mat-select-trigger'))); - selectBox.triggerEventHandler('click', null); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const options: any = fixture.debugElement.queryAll(By.css('mat-option')); - expect(options).not.toBeNull(); - expect(options.length).toBe(4); - expect(options[0].nativeElement.innerText).toContain('ADF.ROLES.SITECOLLABORATOR'); - expect(options[1].nativeElement.innerText).toContain('ADF.ROLES.SITECONSUMER'); - expect(options[2].nativeElement.innerText).toContain('ADF.ROLES.SITECONTRIBUTOR'); - expect(options[3].nativeElement.innerText).toContain('ADF.ROLES.SITEMANAGER'); - }); - }); - })); + it('should toggle the inherited button', async() => { + getNodeSpy.and.returnValue(of(fakeNodeInheritedOnly)); + component.ngOnInit(); + await fixture.detectChanges(); - it('should show the settable roles if the node is not in any site', async(() => { - component.nodeId = 'fake-node-id'; - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithOnlyLocally)); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeEmptyResponse)); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(element.querySelector('#adf-select-role-permission')).toBeDefined(); - expect(element.querySelector('#adf-select-role-permission')).not.toBeNull(); - const selectBox = fixture.debugElement.query(By.css(('#adf-select-role-permission .mat-select-trigger'))); - selectBox.triggerEventHandler('click', null); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const options: any = fixture.debugElement.queryAll(By.css('mat-option')); - expect(options).not.toBeNull(); - expect(options.length).toBe(5); - expect(options[0].nativeElement.innerText).toContain('ADF.ROLES.CONTRIBUTOR'); - expect(options[1].nativeElement.innerText).toContain('ADF.ROLES.COLLABORATOR'); - expect(options[2].nativeElement.innerText).toContain('ADF.ROLES.COORDINATOR'); - expect(options[3].nativeElement.innerText).toContain('ADF.ROLES.EDITOR'); - expect(options[4].nativeElement.innerText).toContain('ADF.ROLES.CONSUMER'); - }); - }); - })); + expect(element.querySelector('.adf-inherit-container .mat-checked')).toBeDefined(); + expect(element.querySelector('.adf-inherit-container h3').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-PERMISSIONS PERMISSION_MANAGER.LABELS.ON'); + expect(element.querySelector('span[title="total"]').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-SUBTITLE'); - it('should update the role when another value is chosen', async(() => { - component.nodeId = 'fake-node-id'; - spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeWithOnlyLocally)); - spyOn(nodeService, 'updateNode').and.returnValue(of({id: 'fake-updated-node'})); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeEmptyResponse)); - component.update.subscribe((updatedPermission) => { - expect(updatedPermission).not.toBeNull(); - expect(updatedPermission.name).toBe('Editor'); - expect(updatedPermission.authorityId).toBe('GROUP_EVERYONE'); - expect(updatedPermission.accessStatus).toBe('ALLOWED'); - }); + spyOn(nodeService, 'updateNode').and.returnValue(of(fakeLocalPermission)); + + const slider = fixture.debugElement.query(By.css('mat-slide-toggle')); + slider.triggerEventHandler('change', { source: { checked: false } }); + await fixture.detectChanges(); + + expect(element.querySelector('.adf-inherit-container .mat-checked')).toBe(null); + expect(element.querySelector('.adf-inherit-container h3').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-PERMISSIONS PERMISSION_MANAGER.LABELS.OFF'); + expect(element.querySelector('span[title="total"]').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-SUBTITLE'); + }); + + it('should not toggle inherited button for read only users', async () => { + getNodeSpy.and.returnValue(of(fakeReadOnlyNodeInherited)); + component.ngOnInit(); + await fixture.detectChanges(); + + expect(element.querySelector('.adf-inherit-container .mat-checked')).toBeDefined(); + expect(element.querySelector('.adf-inherit-container h3').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-PERMISSIONS PERMISSION_MANAGER.LABELS.ON'); + expect(element.querySelector('span[title="total"]').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-SUBTITLE'); + + spyOn(nodeService, 'updateNode').and.returnValue(of(fakeLocalPermission)); + + const slider = fixture.debugElement.query(By.css('mat-slide-toggle')); + slider.triggerEventHandler('change', { source: { checked: false } }); + await fixture.detectChanges(); + + expect(element.querySelector('.adf-inherit-container .mat-checked')).toBeDefined(); + expect(element.querySelector('.adf-inherit-container h3').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-PERMISSIONS PERMISSION_MANAGER.LABELS.ON'); + expect(element.querySelector('span[title="total"]').textContent.trim()) + .toBe('PERMISSION_MANAGER.LABELS.INHERITED-SUBTITLE'); + expect(document.querySelector('simple-snack-bar').textContent) + .toContain('PERMISSION_MANAGER.ERROR.NOT-ALLOWED'); + }); + + }); + + describe('locally set permission', () => { + beforeEach(() => { + getNodeSpy.and.returnValue(of(fakeLocalPermission)); + }); + + it('should show locally set permissions', async() => { + searchQuerySpy.and.returnValue(of(fakeSiteNodeResponse)); + component.ngOnInit(); + + await fixture.detectChanges(); + expect(element.querySelector('adf-user-name-column').textContent).toContain('GROUP_EVERYONE'); + expect(element.querySelector('#adf-select-role-permission').textContent).toContain('Contributor'); + }); + + it('should see the settable roles if the node is not in any site', async() => { + searchQuerySpy.and.returnValue(of(fakeSiteNodeResponse)); + component.ngOnInit(); + + await fixture.detectChanges(); + expect(element.querySelector('adf-user-name-column').textContent).toContain('GROUP_EVERYONE'); + expect(element.querySelector('#adf-select-role-permission').textContent).toContain('Contributor'); + + const selectBox = fixture.debugElement.query(By.css(('[id="adf-select-role-permission"] .mat-select-trigger'))); + selectBox.triggerEventHandler('click', null); fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(element.querySelector('#adf-select-role-permission')).toBeDefined(); - expect(element.querySelector('#adf-select-role-permission')).not.toBeNull(); - const selectBox = fixture.debugElement.query(By.css(('#adf-select-role-permission .mat-select-trigger'))); - selectBox.triggerEventHandler('click', null); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const options: any = fixture.debugElement.queryAll(By.css('mat-option')); - expect(options).not.toBeNull(); - expect(options.length).toBe(5); - options[3].triggerEventHandler('click', {}); - fixture.detectChanges(); - expect(nodeService.updateNode).toHaveBeenCalled(); - }); - }); - })); - }); + + const options: any = fixture.debugElement.queryAll(By.css('mat-option')); + expect(options).not.toBeNull(); + expect(options.length).toBe(4); + expect(options[0].nativeElement.innerText).toContain('ADF.ROLES.SITECOLLABORATOR'); + expect(options[1].nativeElement.innerText).toContain('ADF.ROLES.SITECONSUMER'); + expect(options[2].nativeElement.innerText).toContain('ADF.ROLES.SITECONTRIBUTOR'); + expect(options[3].nativeElement.innerText).toContain('ADF.ROLES.SITEMANAGER'); + }); + + it('should update the role when another value is chosen', async () => { + spyOn(nodeService, 'updateNode').and.returnValue(of({id: 'fake-uwpdated-node'})); + searchQuerySpy.and.returnValue(of(fakeEmptyResponse)); + component.ngOnInit(); + + await fixture.detectChanges(); + + expect(element.querySelector('adf-user-name-column').textContent).toContain('GROUP_EVERYONE'); + expect(element.querySelector('#adf-select-role-permission').textContent).toContain('Contributor'); + + const selectBox = fixture.debugElement.query(By.css(('[id="adf-select-role-permission"] .mat-select-trigger'))); + selectBox.triggerEventHandler('click', null); + fixture.detectChanges(); + const options: any = fixture.debugElement.queryAll(By.css('mat-option')); + expect(options).not.toBeNull(); + expect(options.length).toBe(5); + options[3].triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(nodeService.updateNode).toHaveBeenCalledWith('f472543f-7218-403d-917b-7a5861257244', { permissions: { locallySet: [ { accessStatus: 'ALLOWED', name: 'Editor', authorityId: 'GROUP_EVERYONE' } ] } }); + }); + + it('should delete the person', async () => { + spyOn(nodeService, 'updateNode').and.returnValue(of({id: 'fake-uwpdated-node'})); + searchQuerySpy.and.returnValue(of(fakeEmptyResponse)); + component.ngOnInit(); + await fixture.detectChanges(); + + expect(element.querySelector('adf-user-name-column').textContent).toContain('GROUP_EVERYONE'); + expect(element.querySelector('#adf-select-role-permission').textContent).toContain('Contributor'); + + const showButton: HTMLButtonElement = element.querySelector('[data-automation-id="adf-delete-permission-button-GROUP_EVERYONE"]'); + showButton.click(); + fixture.detectChanges(); + + expect(nodeService.updateNode).toHaveBeenCalledWith('f472543f-7218-403d-917b-7a5861257244', { permissions: { locallySet: [ ] } }); + }); + + }); }); diff --git a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.ts b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.ts index 0a0ac23f9d..9ca9b85703 100644 --- a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.ts +++ b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.ts @@ -15,11 +15,11 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, Input, OnInit, EventEmitter, Output } from '@angular/core'; -import { NodesApiService } from '@alfresco/adf-core'; -import { Node, PermissionElement } from '@alfresco/js-api'; +import { ObjectDataRow } from '@alfresco/adf-core'; +import { PermissionElement } from '@alfresco/js-api'; +import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; import { PermissionDisplayModel } from '../../models/permission.model'; -import { NodePermissionService } from '../../services/node-permission.service'; +import { PermissionListService } from './permission-list.service'; @Component({ selector: 'adf-permission-list', @@ -27,70 +27,54 @@ import { NodePermissionService } from '../../services/node-permission.service'; styleUrls: ['./permission-list.component.scss'], encapsulation: ViewEncapsulation.None }) -export class PermissionListComponent implements OnInit { - +export class PermissionListComponent { /** ID of the node whose permissions you want to show. */ @Input() - nodeId: string = ''; + nodeId: string; /** Emitted when the permission is updated. */ @Output() - update = new EventEmitter(); + update: EventEmitter; /** Emitted when an error occurs. */ @Output() - error = new EventEmitter(); + error: EventEmitter; - permissionList: PermissionDisplayModel[]; - settableRoles: any[]; - actualNode: Node; + selectedPermissions: PermissionDisplayModel[] = []; - constructor(private nodeService: NodesApiService, - private nodePermissionService: NodePermissionService) { + constructor(public readonly permissionList: PermissionListService) { + this.error = this.permissionList.errored; + this.update = this.permissionList.updated; } - ngOnInit() { - this.fetchNodePermissions(); + ngOnInit(): void { + this.permissionList.fetchPermission(this.nodeId); } - reload() { - this.fetchNodePermissions(); + openAddPermissionDialog() { + this.permissionList.updateNodePermissionByDialog(); } - private fetchNodePermissions() { - this.nodeService.getNode(this.nodeId).subscribe((node: Node) => { - this.actualNode = node; - this.permissionList = this.nodePermissionService.getNodePermissions(node); - - this.nodePermissionService.getNodeRoles(node).subscribe((settableList: string[]) => { - this.settableRoles = settableList; - }); - }); + onSelect(selections: ObjectDataRow[]) { + this.selectedPermissions = selections.map((selection) => selection['obj']); } - saveNewRole(event: any, permissionRow: PermissionDisplayModel) { - const updatedPermissionRole = this.buildUpdatedPermission(event.value, permissionRow); - - this.nodePermissionService.updatePermissionRole(this.actualNode, updatedPermissionRole) - .subscribe(() => { - this.update.emit(updatedPermissionRole); - }); + deleteSelection() { + this.permissionList.deletePermissions(this.selectedPermissions); + this.selectedPermissions = []; } - private buildUpdatedPermission(newRole: string, permissionRow: PermissionDisplayModel): PermissionElement { - return { - accessStatus: permissionRow.accessStatus, - name: newRole, - authorityId: permissionRow.authorityId - }; + updatePermission({role, permission}) { + this.permissionList.updateRole(role, permission); } - removePermission(permissionRow: PermissionDisplayModel) { - this.nodePermissionService - .removePermission(this.actualNode, permissionRow) - .subscribe( - node => this.update.emit(node), - error => this.error.emit(error) - ); + deletePermission(permission: PermissionDisplayModel) { + this.selectedPermissions = []; + this.permissionList.deletePermission(permission); + } + + updateAllPermission(role: string) { + this.permissionList.bulkRoleUpdate(role); + this.selectedPermissions = []; } } diff --git a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.spec.ts b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.spec.ts new file mode 100644 index 0000000000..c02fde694c --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.spec.ts @@ -0,0 +1,149 @@ +/*! + * @license + * Copyright 2019 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 { NodesApiService, NotificationService, setupTestBed } from '@alfresco/adf-core'; +import { TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of, throwError } from 'rxjs'; +import { PermissionListService } from './permission-list.service'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { NodePermissionService } from '../../services/node-permission.service'; +import { fakeNodeInheritedOnly, fakeNodeWithOnlyLocally } from '../../../mock/permission-list.component.mock'; +import { PermissionDisplayModel } from '../../models/permission.model'; + +describe('PermissionListService', () => { + let service: PermissionListService; + let nodePermissionService: NodePermissionService; + let notificationService: NotificationService; + let nodesApiService: NodesApiService; + const localPermission = [new PermissionDisplayModel({ + authorityId: 'GROUP_EVERYONE', + name: 'Contributor', + accessStatus: 'ALLOWED' + }) + ]; + + setupTestBed({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + beforeEach(() => { + service = TestBed.inject(PermissionListService); + nodePermissionService = TestBed.inject(NodePermissionService); + notificationService = TestBed.inject(NotificationService); + nodesApiService = TestBed.inject(NodesApiService); + spyOn(notificationService, 'showInfo').and.stub(); + spyOn(notificationService, 'showWarning').and.stub(); + spyOn(notificationService, 'showError').and.stub(); + }); + + it('fetch Permission', (done) => { + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({node: fakeNodeWithOnlyLocally , roles: []})); + + const subscription = service.data$.subscribe(({ node, inheritedPermissions, localPermissions, roles }) => { + expect(node).toBe(fakeNodeWithOnlyLocally); + expect(inheritedPermissions).toEqual([]); + expect(roles).toEqual([]); + expect(localPermissions).toEqual(localPermission); + subscription.unsubscribe(); + done(); + }); + + service.fetchPermission('fake node'); + }); + + describe('toggle permission', () => { + + it('should show error if user doesn\'t have permission to update node', () => { + const node = JSON.parse(JSON.stringify(fakeNodeInheritedOnly)), event = { source: { checked: false } }; + node.allowableOperations = []; + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({node , roles: []})); + spyOn(nodesApiService, 'updateNode').and.stub(); + service.fetchPermission('fetch node'); + service.toggleInherited(event as any); + expect(nodesApiService.updateNode).not.toHaveBeenCalled(); + expect(notificationService.showError).toHaveBeenCalledWith('PERMISSION_MANAGER.ERROR.NOT-ALLOWED'); + }); + + it('should show message after success toggle', () => { + const node = JSON.parse(JSON.stringify(fakeNodeInheritedOnly)), event = { source: { checked: false } }; + const updateNode = JSON.parse(JSON.stringify(fakeNodeInheritedOnly)); + updateNode.permissions.isInheritanceEnabled = false; + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({node , roles: []})); + spyOn(nodesApiService, 'updateNode').and.returnValue(of(updateNode)); + service.fetchPermission('fetch node'); + + service.toggleInherited(event as any); + expect(nodesApiService.updateNode).toHaveBeenCalled(); + expect(notificationService.showInfo).toHaveBeenCalledWith('PERMISSION_MANAGER.MESSAGE.INHERIT-DISABLE-SUCCESS'); + }); + + it('should show message for errored toggle', () => { + const node = JSON.parse(JSON.stringify(fakeNodeInheritedOnly)), event = { source: { checked: false } }; + spyOn(nodesApiService, 'updateNode').and.returnValue(throwError('Failed to update')); + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({node , roles: []})); + service.fetchPermission('fetch node'); + + service.toggleInherited(event as any); + expect(nodesApiService.updateNode).toHaveBeenCalled(); + expect(notificationService.showWarning).toHaveBeenCalledWith('PERMISSION_MANAGER.MESSAGE.TOGGLE-PERMISSION-FAILED'); + }); + }); + + describe('delete permission', () => { + const node = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); + beforeEach(() => { + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({node , roles: []})); + service.fetchPermission('fetch node'); + }); + + it('should be able to delete a permission', () => { + spyOn(nodePermissionService, 'removePermissions').and.returnValue(of(node)); + service.deletePermissions(localPermission); + expect(notificationService.showInfo).toHaveBeenCalledWith('PERMISSION_MANAGER.MESSAGE.PERMISSION-BULK-DELETE-SUCCESS', null, { user: 0, group: 1 }); + }); + + it('should show error message for errored delete operation', () => { + spyOn(nodePermissionService, 'removePermissions').and.returnValue(throwError('Failed operation')); + service.deletePermissions(localPermission); + expect(notificationService.showError).toHaveBeenCalledWith('PERMISSION_MANAGER.MESSAGE.PERMISSION-DELETE-FAIL'); + }); + }); + + describe('Bulk Role', () => { + const node = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); + beforeEach(() => { + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({node , roles: []})); + service.fetchPermission('fetch node'); + }); + + it('should be able to update bulk permission', () => { + spyOn(nodePermissionService, 'updatePermissions').and.returnValue(of(node)); + service.bulkRoleUpdate('fake-role'); + expect(notificationService.showInfo).toHaveBeenCalledWith('PERMISSION_MANAGER.MESSAGE.PERMISSION-BULK-UPDATE-SUCCESS', null, { user: 0, group: 1 }); + }); + + it('should show error message for errored operation', () => { + spyOn(nodePermissionService, 'updatePermissions').and.returnValue(throwError('Error')); + service.bulkRoleUpdate('fake-role'); + expect(notificationService.showError).toHaveBeenCalledWith('PERMISSION_MANAGER.MESSAGE.PERMISSION-UPDATE-FAIL'); + }); + }); +}); diff --git a/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.ts b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.ts new file mode 100644 index 0000000000..7b2ab88de7 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.service.ts @@ -0,0 +1,207 @@ +/*! + * @license + * Copyright 2019 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 { AllowableOperationsEnum, ContentService, NodesApiService, NotificationService } from '@alfresco/adf-core'; +import { Node, PermissionElement } from '@alfresco/js-api'; +import { EventEmitter, Injectable } from '@angular/core'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; +import { BehaviorSubject, forkJoin, Observable, of, Subject } from 'rxjs'; +import { finalize, map, switchMap } from 'rxjs/operators'; +import { RoleModel } from '../../models/role.model'; +import { PermissionDisplayModel } from '../../models/permission.model'; +import { NodePermissionsModel } from '../../models/member.model'; +import { NodePermissionService } from '../../services/node-permission.service'; +import { NodePermissionDialogService } from '../../services/node-permission-dialog.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PermissionListService { + updated = new EventEmitter(); + errored = new EventEmitter(); + + loading$: BehaviorSubject = new BehaviorSubject(true); + error$: Subject = new Subject(); + nodeWithRoles$: Subject<{ node: Node, roles: RoleModel[] }> = new Subject(); + data$: Observable = this.nodeWithRoles$.pipe( + map(({ node, roles}) => { + return { + node, + roles, + inheritedPermissions: this.nodePermissionService.getInheritedPermission(node), + localPermissions: this.nodePermissionService.getLocalPermissions(node), + allPermission: this.nodePermissionService.getNodePermissions(node) + }; + }) + ); + + private node: Node; + private roles: RoleModel[]; + + constructor( + private nodeService: NodesApiService, + private nodePermissionService: NodePermissionService, + private nodePermissionDialogService: NodePermissionDialogService, + private contentService: ContentService, + private notificationService: NotificationService + ) {} + + fetchPermission(nodeId: string) { + this.loading$.next(true); + this.nodePermissionService.getNodeWithRoles(nodeId) + .pipe(finalize(() => this.loading$.next(false))) + .subscribe( + ({ node, roles }) => { + this.node = node; + this.roles = roles; + this.nodeWithRoles$.next({ node, roles }); + }, + () => this.error$.next(true) + ); + } + + toggleInherited(change: MatSlideToggleChange) { + if (this.contentService.hasAllowableOperations(this.node, AllowableOperationsEnum.UPDATEPERMISSIONS)) { + const nodeBody = { + permissions: { + isInheritanceEnabled: !this.node.permissions.isInheritanceEnabled + } + }; + this.nodeService.updateNode(this.node.id, nodeBody, {include: ['permissions']}) + .subscribe( + (nodeUpdated: Node) => { + const message = nodeUpdated.permissions.isInheritanceEnabled ? 'PERMISSION_MANAGER.MESSAGE.INHERIT-ENABLE-SUCCESS' : 'PERMISSION_MANAGER.MESSAGE.INHERIT-DISABLE-SUCCESS'; + this.notificationService.showInfo(message); + nodeUpdated.permissions.inherited = nodeUpdated.permissions?.inherited ?? []; + this.reloadNode(nodeUpdated); + }, + () => { + change.source.checked = this.node.permissions.isInheritanceEnabled; + this.notificationService.showWarning('PERMISSION_MANAGER.MESSAGE.TOGGLE-PERMISSION-FAILED'); + } + ); + } else { + change.source.checked = this.node.permissions.isInheritanceEnabled; + this.notificationService.showError('PERMISSION_MANAGER.ERROR.NOT-ALLOWED'); + } + } + + updateNodePermissionByDialog() { + this.nodePermissionDialogService + .openAddPermissionDialog(this.node, this.roles) + .pipe( + switchMap(selection => { + const total = selection.length; + const group = selection.filter(({authorityId}) => this.isGroup(authorityId)).length; + return forkJoin({ + user: of(total - group), + group: of(group), + node: this.nodePermissionService.updateNodePermissions(this.node.id, selection) + }); + }) + ) + .subscribe(({ user, group, node}) => { + this.notificationService.showInfo( 'PERMISSION_MANAGER.MESSAGE.PERMISSION-ADD-SUCCESS', null, { user, group }); + this.reloadNode(node); + }, + () => { + this.notificationService.showError( 'PERMISSION_MANAGER.MESSAGE.PERMISSION-ADD-FAIL'); + this.reloadNode(); + } + ); + } + + deletePermissions(permissions: PermissionElement[]) { + this.nodePermissionService.removePermissions(this.node, permissions) + .subscribe((node) => { + const total = permissions.length; + const group = permissions.filter(({authorityId}) => this.isGroup(authorityId)).length; + this.notificationService.showInfo('PERMISSION_MANAGER.MESSAGE.PERMISSION-BULK-DELETE-SUCCESS', null, {user: total - group, group}); + this.reloadNode(node); + }, + () => { + this.notificationService.showError('PERMISSION_MANAGER.MESSAGE.PERMISSION-DELETE-FAIL'); + this.reloadNode(); + } + ); + } + + updateRole(role: string, permission: PermissionDisplayModel) { + const updatedPermissionRole = this.buildUpdatedPermission(role, permission); + this.nodePermissionService.updatePermissionRole(this.node, updatedPermissionRole) + .subscribe((node) => { + this.notificationService.showInfo('PERMISSION_MANAGER.MESSAGE.PERMISSION-UPDATE-SUCCESS'); + this.reloadNode(node); + this.updated.emit(permission); + }, + () => { + this.notificationService.showError('PERMISSION_MANAGER.MESSAGE.PERMISSION-UPDATE-FAIL'); + this.reloadNode(); + this.errored.emit(permission); + } + ); + } + + bulkRoleUpdate(role: string) { + const permissions = [...this.node.permissions.locallySet] .map((permission) => this.buildUpdatedPermission(role, permission)); + this.nodePermissionService.updatePermissions(this.node, permissions) + .subscribe((node) => { + const total = permissions.length; + const group = permissions.filter(({authorityId}) => this.isGroup(authorityId)).length; + this.notificationService.showInfo('PERMISSION_MANAGER.MESSAGE.PERMISSION-BULK-UPDATE-SUCCESS', null, {user: total - group, group}); + this.reloadNode(node); + }, + () => { + this.notificationService.showError('PERMISSION_MANAGER.MESSAGE.PERMISSION-UPDATE-FAIL'); + this.reloadNode(); + } + ); + } + + deletePermission(permission: PermissionDisplayModel) { + this.nodePermissionService + .removePermission(this.node, permission) + .subscribe((node) => { + this.notificationService.showInfo('PERMISSION_MANAGER.MESSAGE.PERMISSION-DELETE-SUCCESS'); + this.reloadNode(node); + }, + () => { + this.notificationService.showError('PERMISSION_MANAGER.MESSAGE.PERMISSION-DELETE-FAIL'); + this.reloadNode(); + } + ); + } + + private buildUpdatedPermission(role: string, permission: PermissionElement): PermissionElement { + return { + accessStatus: permission.accessStatus, + name: role, + authorityId: permission.authorityId + }; + } + + private reloadNode(node?: Node) { + if (node != null) { + Object.assign(this.node.permissions, node.permissions); + } + this.nodeWithRoles$.next({ node: this.node, roles: this.roles }); + } + + private isGroup(authorityId) { + return authorityId.startsWith('GROUP_') || authorityId.startsWith('ROLE_'); + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/pop-over.directive.ts b/lib/content-services/src/lib/permission-manager/components/pop-over.directive.ts new file mode 100644 index 0000000000..7fc50b7518 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/pop-over.directive.ts @@ -0,0 +1,104 @@ +/*! + * @license + * Copyright 2019 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 { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; +import { ConnectionPositionPair, Overlay, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; + +@Directive({ + selector: '[adf-pop-over]', + exportAs: 'adfPopOver' + +}) +export class PopOverDirective implements OnInit, OnDestroy, AfterViewInit { + get open(): boolean { + return this._open; + } + + @Input('adf-pop-over') popOver!: TemplateRef; + @Input() target!: HTMLElement; + @Input() panelClass = 'adf-permission-pop-over'; + + private _open = false; + private destroy$ = new Subject(); + private overlayRef!: OverlayRef; + + constructor( + private element: ElementRef, + private overlay: Overlay, + private vcr: ViewContainerRef + ) { } + + ngOnInit(): void { + this.createOverlay(); + } + + ngAfterViewInit(): void { + this.element.nativeElement.addEventListener('click', () => this.attachOverlay()); + } + + ngOnDestroy(): void { + this.detachOverlay(); + this.destroy$.next(); + this.destroy$.complete(); + } + + private createOverlay(): void { + const scrollStrategy = this.overlay.scrollStrategies.reposition(); + const positionStrategy = this.overlay + .position() + .flexibleConnectedTo(this.target) + .withPositions([ + new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }), + new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }) + ]) + .withPush(false); + + this.overlayRef = this.overlay.create({ + positionStrategy, + scrollStrategy, + hasBackdrop: true, + backdropClass: 'cdk-overlay-transparent-backdrop', + panelClass: this.panelClass + }); + + this.overlayRef + .backdropClick() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this._open = false; + this.detachOverlay(); + }); + } + + private attachOverlay(): void { + if (!this.overlayRef.hasAttached()) { + const periodSelectorPortal = new TemplatePortal(this.popOver, this.vcr); + + this.overlayRef.attach(periodSelectorPortal); + this._open = true; + } + } + + private detachOverlay(): void { + if (this.overlayRef.hasAttached()) { + this.overlayRef.detach(); + } + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.scss b/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.scss new file mode 100644 index 0000000000..677acc387e --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.scss @@ -0,0 +1,34 @@ +@mixin adf-user-icon-column-theme($theme) { + $primary: map-get($theme, primary); + $foreground: map-get($theme, foreground); + + .adf { + &-people-initial { + background: mat-color($primary); + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + width: 40px; + height: 40px; + color: mat-color($foreground, text); + font-weight: bolder; + font-size: 18px; + text-transform: uppercase; + } + + &-people-icon { + height: 20px !important; + width: 20px !important; + background: mat-color($primary); + border-radius: 50%; + padding: 10px; + color: mat-color($foreground, text); + font-weight: bolder; + font-size: 20px; + } + &-people-select-icon { + padding: 10px; + } + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.spec.ts b/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.spec.ts new file mode 100644 index 0000000000..b0ff0d9e7c --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.spec.ts @@ -0,0 +1,118 @@ +/*! + * @license + * Copyright 2019 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 { setupTestBed } from '@alfresco/adf-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { UserIconColumnComponent } from './user-icon-column.component'; +import { NodeEntry } from '@alfresco/js-api'; + +describe('UserIconColumnComponent', () => { + + let fixture: ComponentFixture; + let component: UserIconColumnComponent; + let element: HTMLElement; + const person = { + firstName: 'fake', + lastName: 'user', + email: 'fake@test.com' + }; + + const group = { + id: 'fake-id', + displayName: 'fake authority' + }; + + setupTestBed({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserIconColumnComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + fixture.detectChanges(); + }); + + describe('person initial', () => { + it('should render person value from context', () => { + component.context = { + row: { + obj: { + person + } + } + }; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[id="user-initials-image"]').textContent).toContain('fu'); + }); + + it('should render person value from node', () => { + component.node = { + entry: { + nodeType: 'cm:person', + properties: { + 'cm:firstName': 'Fake', + 'cm:lastName': 'User', + 'cm:email': 'fake-user@test.com', + 'cm:userName': 'fake-user' + } + } + } as NodeEntry; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[id="user-initials-image"]').textContent).toContain('FU'); + }); + }); + + describe('group initial', () => { + it('should render group value from context', () => { + component.context = { + row: { + obj: { + group + } + } + }; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[id="group-icon"] mat-icon')).toBeDefined(); + expect(element.querySelector('[id="group-icon"] mat-icon').textContent).toContain('people_alt_outline'); + }); + + it('should render person value from node', () => { + component.node = { + entry: { + nodeType: 'cm:authorityContainer', + properties: { + 'cm:authorityName': 'Fake authorityN' + } + } + } as NodeEntry; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[id="group-icon"] mat-icon')).toBeDefined(); + expect(element.querySelector('[id="group-icon"] mat-icon').textContent).toContain('people_alt_outline'); + }); + }); + +}); diff --git a/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.ts b/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.ts new file mode 100644 index 0000000000..9de96ebaf6 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/user-icon-column/user-icon-column.component.ts @@ -0,0 +1,69 @@ +/*! + * @license + * Copyright 2019 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 { User } from '@alfresco/adf-core'; +import { NodeEntry } from '@alfresco/js-api'; +import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { NodePermissionService } from '../../services/node-permission.service'; + +@Component({ + selector: 'adf-user-icon-column', + template: ` +
+ + people_alt_outline +
+
+
+
+ +
+ `, + styleUrls: ['./user-icon-column.component.scss'], + host: { class: 'adf-user-icon-column adf-datatable-content-cell' } +}) +export class UserIconColumnComponent implements OnInit { + @Input() + context: any; + + @Input() + node: NodeEntry; + + displayText$ = new BehaviorSubject(null); + group = false; + + constructor(private nodePermissionService: NodePermissionService) {} + + ngOnInit() { + if (this.context) { + const { person, group, authorityId } = this.context.row.obj?.entry ?? this.context.row.obj; + this.group = this.isGroup(group, authorityId); + this.displayText$.next(person || group || { displayName: authorityId }); + } + + if (this.node) { + const { person, group } = this.nodePermissionService.transformNodeToUserPerson(this.node.entry); + this.group = this.isGroup(group, null); + this.displayText$.next(person || group); + } + } + + private isGroup(group, authorityId): boolean { + return !!group || authorityId?.startsWith('GROUP_') || authorityId?.startsWith('ROLE_'); + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.scss b/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.scss new file mode 100644 index 0000000000..9467826782 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.scss @@ -0,0 +1,24 @@ +@mixin member-theme($theme) { + $primary: map-get($theme, primary); + $foreground: map-get($theme, foreground); + + .adf-user { + display: flex; + flex-direction: column; + + &-name-column { + padding: 2px 10px; + font-weight: 600; + font-size: 14px; + } + + &-email-column { + padding: 2px 10px; + font-size: 14px; + letter-spacing: -0.2px; + line-height: 1.43; + color: mat-color($foreground, text, 0.72); + } + + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.spec.ts b/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.spec.ts new file mode 100644 index 0000000000..db85013079 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.spec.ts @@ -0,0 +1,136 @@ +/*! + * @license + * Copyright 2019 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 { setupTestBed } from '@alfresco/adf-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { UserNameColumnComponent } from './user-name-column.component'; +import { NodeEntry } from '@alfresco/js-api'; + +describe('UserNameColumnComponent', () => { + + let fixture: ComponentFixture; + let component: UserNameColumnComponent; + let element: HTMLElement; + const person = { + firstName: 'fake', + lastName: 'user', + email: 'fake@test.com' + }; + + const group = { + id: 'fake-id', + displayName: 'fake authority' + }; + + setupTestBed({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserNameColumnComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + fixture.detectChanges(); + }); + + describe('person name', () => { + it('should render person value from context', () => { + component.context = { + row: { + obj: { + person + } + } + }; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[title="fake user"]').textContent).toContain('fake user'); + expect(element.querySelector('[title="fake@test.com"]').textContent).toContain('fake@test.com'); + }); + + it('should render person value from node', (done) => { + component.node = { + entry: { + nodeType: 'cm:person', + properties: { + 'cm:firstName': 'Fake', + 'cm:lastName': 'User', + 'cm:email': 'fake-user@test.com', + 'cm:userName': 'fake-user' + } + } + } as NodeEntry; + + const subscription = component.displayText$.subscribe((fullName) => { + if (fullName) { + expect(fullName).toBe('Fake User'); + subscription.unsubscribe(); + done(); + } + }); + + component.ngOnInit(); + }); + }); + + describe('group name', () => { + it('should render group value from context', () => { + component.context = { + row: { + obj: { + group + } + } + }; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[title="fake authority"]').textContent.trim()).toBe('fake authority'); + }); + + it('should render group for authorityId', () => { + component.context = { + row: { + obj: { + authorityId: 'fake-id' + } + } + }; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[title=fake-id]').textContent.trim()).toBe('fake-id'); + }); + + it('should render person value from node', () => { + component.node = { + entry: { + nodeType: 'cm:authorityContainer', + properties: { + 'cm:authorityName': 'Fake authority' + } + } + } as NodeEntry; + component.ngOnInit(); + fixture.detectChanges(); + expect(element.querySelector('[title="Fake authority"]').textContent.trim()).toBe('Fake authority'); + }); + }); +}); diff --git a/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.ts b/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.ts new file mode 100644 index 0000000000..38ff2aa7c7 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/user-name-column/user-name-column.component.ts @@ -0,0 +1,74 @@ +/*! + * @license + * Copyright 2019 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, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Group, NodeEntry, Person } from '@alfresco/js-api'; +import { NodePermissionService } from '../../services/node-permission.service'; + +@Component({ + selector: 'adf-user-name-column', + template: ` +
+ {{ displayText$ | async }} +
+ {{ subTitleText$ | async }} +
+ `, + host: { class: 'adf-user-name-column adf-datatable-content-cell adf-expand-cell-5 adf-ellipsis-cell' }, + styleUrls: [ './user-name-column.component.scss' ] +}) +export class UserNameColumnComponent implements OnInit { + @Input() + context: any; + + @Input() + node: NodeEntry; + + displayText$ = new BehaviorSubject(''); + subTitleText$ = new BehaviorSubject(''); + + constructor(private nodePermissionService: NodePermissionService) {} + + ngOnInit() { + if (this.context != null) { + const { person, group, authorityId } = this.context.row.obj?.entry ?? this.context.row.obj; + const permissionGroup = authorityId ? { displayName: authorityId } as Group : null; + this.updatePerson(person); + this.updateGroup(group || permissionGroup); + } + + if (this.node) { + const { person, group } = this.nodePermissionService.transformNodeToUserPerson(this.node.entry); + this.updatePerson(person); + this.updateGroup(group); + } + } + + private updatePerson(person: Person) { + if (person) { + this.displayText$.next(`${person.firstName ?? ''} ${person.lastName ?? ''}`); + this.subTitleText$.next(person.email ?? ''); + } + } + + private updateGroup(group: Group) { + if (group) { + this.displayText$.next(group.displayName); + } + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/user-role-column/user-role-column.component.ts b/lib/content-services/src/lib/permission-manager/components/user-role-column/user-role-column.component.ts new file mode 100644 index 0000000000..8cca527e2d --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/components/user-role-column/user-role-column.component.ts @@ -0,0 +1,69 @@ +/*! + * @license + * Copyright 2019 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, EventEmitter, Input, Output } from '@angular/core'; +import { RoleModel } from '../../models/role.model'; + +@Component({ + selector: 'adf-user-role-column', + template: ` + + + + {{ role.label | adfLocalizedRole }} + + + + + + {{value | adfLocalizedRole}} + + `, + host: { class: 'adf-user-role-column adf-datatable-content-cell adf-expand-cell-4' }, + styles: [ + `.adf-role-selector-field { + width: 100%; + } + ` + ] +}) +export class UserRoleColumnComponent { + + @Input() + roles: RoleModel[]; + + @Input() + value: string; + + @Input() + readonly = false; + + @Input() + placeholder: string = 'PERMISSION_MANAGER.LABELS.SELECT-ROLE'; + + @Output() + roleChanged: EventEmitter = new EventEmitter(); + + onRoleChanged(newRole: string) { + this.value = newRole; + this.roleChanged.emit(newRole); + } +} diff --git a/lib/content-services/src/lib/permission-manager/models/member.model.ts b/lib/content-services/src/lib/permission-manager/models/member.model.ts new file mode 100644 index 0000000000..336b4215e5 --- /dev/null +++ b/lib/content-services/src/lib/permission-manager/models/member.model.ts @@ -0,0 +1,85 @@ +/*! + * @license + * Copyright 2019 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 { Group, Node, NodeEntry, PermissionElement, Person } from '@alfresco/js-api'; +import { PermissionDisplayModel } from './permission.model'; +import { RoleModel } from './role.model'; + +export interface NodePermissionsModel { + node: Node; + roles: RoleModel[]; + inheritedPermissions: PermissionDisplayModel[]; + localPermissions: PermissionDisplayModel[]; +} + +export class MemberModel { + id: string; + role: string; + accessStatus: PermissionElement.AccessStatusEnum | string; + entry: { + person?: Person; + group?: Group; + }; + readonly: boolean = false; + + constructor(input?) { + if (input) { + Object.assign(this, input); + } + } + + static parseFromSearchResult({ entry }: NodeEntry): MemberModel { + const result = new MemberModel(); + + if (entry.nodeType === 'cm:person') { + const person = new Person({ + firstName: entry.properties['cm:firstName'], + lastName: entry.properties['cm:lastName'], + email: entry.properties['cm:email'], + id: entry.properties['cm:userName'] + }); + + result.id = person.id; + result.entry = { person }; + result.accessStatus = 'ALLOWED'; + + return result; + } + + if (entry.nodeType === 'cm:authorityContainer') { + const group = new Group({ + id: entry.properties['cm:authorityName'], + displayName: entry.properties['cm:authorityDisplayName'] || entry.properties['cm:authorityName'] + }); + + result.id = group.id; + result.entry = { group }; + result.accessStatus = 'ALLOWED'; + + return result; + } + return null; + } + + toPermissionElement(): PermissionElement { + return { + authorityId: this.id, + name: this.role, + accessStatus: this.accessStatus + }; + } +} diff --git a/lib/content-services/src/lib/permission-manager/components/permission-list/no-permission.component.ts b/lib/content-services/src/lib/permission-manager/models/role.model.ts similarity index 72% rename from lib/content-services/src/lib/permission-manager/components/permission-list/no-permission.component.ts rename to lib/content-services/src/lib/permission-manager/models/role.model.ts index 62d7ccd1e7..1326695e90 100644 --- a/lib/content-services/src/lib/permission-manager/components/permission-list/no-permission.component.ts +++ b/lib/content-services/src/lib/permission-manager/models/role.model.ts @@ -15,12 +15,7 @@ * limitations under the License. */ -/* tslint:disable:no-input-rename */ - -import { Component } from '@angular/core'; - -@Component({ - selector: 'adf-no-permission-template', - template: '' -}) -export class NoPermissionTemplateComponent {} +export interface RoleModel { + label: string; + role: string; +} diff --git a/lib/content-services/src/lib/permission-manager/permission-manager.module.ts b/lib/content-services/src/lib/permission-manager/permission-manager.module.ts index be76e36776..943268c146 100644 --- a/lib/content-services/src/lib/permission-manager/permission-manager.module.ts +++ b/lib/content-services/src/lib/permission-manager/permission-manager.module.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { CoreModule, PipeModule } from '@alfresco/adf-core'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -22,11 +23,15 @@ 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 { CoreModule, PipeModule } from '@alfresco/adf-core'; import { InheritPermissionDirective } from './components/inherited-button.directive'; -import { NoPermissionTemplateComponent } from './components/permission-list/no-permission.component'; import { AddPermissionPanelComponent } from './components/add-permission/add-permission-panel.component'; import { SearchModule } from '../search/search.module'; +import { UserNameColumnComponent } from './components/user-name-column/user-name-column.component'; +import { UserIconColumnComponent } from './components/user-icon-column/user-icon-column.component'; +import { UserRoleColumnComponent } from './components/user-role-column/user-role-column.component'; +import { NodePathColumnComponent } from './components/node-path-column/node-path-column.component'; +import { PopOverDirective } from './components/pop-over.directive'; +import { PermissionContainerComponent } from './components/permission-container/permission-container.component'; @NgModule({ imports: [ @@ -40,19 +45,28 @@ import { SearchModule } from '../search/search.module'; ], declarations: [ PermissionListComponent, - NoPermissionTemplateComponent, AddPermissionPanelComponent, InheritPermissionDirective, AddPermissionComponent, - AddPermissionDialogComponent + AddPermissionDialogComponent, + UserNameColumnComponent, + UserIconColumnComponent, + UserRoleColumnComponent, + PopOverDirective, + NodePathColumnComponent, + PermissionContainerComponent ], exports: [ PermissionListComponent, - NoPermissionTemplateComponent, AddPermissionPanelComponent, InheritPermissionDirective, AddPermissionComponent, - AddPermissionDialogComponent + AddPermissionDialogComponent, + UserNameColumnComponent, + UserIconColumnComponent, + UserRoleColumnComponent, + PopOverDirective, + NodePathColumnComponent ] }) export class PermissionManagerModule {} diff --git a/lib/content-services/src/lib/permission-manager/public-api.ts b/lib/content-services/src/lib/permission-manager/public-api.ts index 58e93a8b98..0b09337e2d 100644 --- a/lib/content-services/src/lib/permission-manager/public-api.ts +++ b/lib/content-services/src/lib/permission-manager/public-api.ts @@ -16,7 +16,6 @@ */ export * from './components/permission-list/permission-list.component'; -export * from './components/permission-list/no-permission.component'; export * from './components/inherited-button.directive'; export * from './models/permission.model'; export * from './services/node-permission-dialog.service'; @@ -26,5 +25,13 @@ 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 './components/add-permission/search-config-permission.service'; +export * from './components/user-icon-column/user-icon-column.component'; +export * from './components/user-name-column/user-name-column.component'; +export * from './components/user-role-column/user-role-column.component'; +export * from './components/node-path-column/node-path-column.component'; +export * from './components/permission-container/permission-container.component'; +export * from './components/pop-over.directive'; +export * from './models/member.model'; +export * from './models/role.model'; export * from './permission-manager.module'; diff --git a/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.spec.ts b/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.spec.ts index b082f22c0c..b1c4cea9fe 100644 --- a/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.spec.ts +++ b/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.spec.ts @@ -16,10 +16,10 @@ */ import { TestBed } from '@angular/core/testing'; -import { AppConfigService, setupTestBed, ContentService } from '@alfresco/adf-core'; +import { AppConfigService, setupTestBed } from '@alfresco/adf-core'; import { NodePermissionDialogService } from './node-permission-dialog.service'; import { MatDialog } from '@angular/material/dialog'; -import { Subject, of, throwError } from 'rxjs'; +import { of, Subject, throwError } from 'rxjs'; import { ContentTestingModule } from '../../testing/content.testing.module'; import { NodePermissionService } from './node-permission.service'; import { Node } from '@alfresco/js-api'; @@ -32,7 +32,6 @@ describe('NodePermissionDialogService', () => { let spyOnDialogOpen: jasmine.Spy; let afterOpenObservable: Subject; let nodePermissionService: NodePermissionService; - let contentService: ContentService; setupTestBed({ imports: [ @@ -48,7 +47,6 @@ describe('NodePermissionDialogService', () => { materialDialog = TestBed.inject(MatDialog); afterOpenObservable = new Subject(); nodePermissionService = TestBed.inject(NodePermissionService); - contentService = TestBed.inject(ContentService); spyOnDialogOpen = spyOn(materialDialog, 'open').and.returnValue({ afterOpen: () => afterOpenObservable, afterClosed: () => of({}), @@ -67,14 +65,14 @@ describe('NodePermissionDialogService', () => { }); it('should be able to open the dialog showing node permissions', () => { - service.openAddPermissionDialog(fakePermissionNode, 'fake-title'); + service.openAddPermissionDialog(fakePermissionNode, [], 'fake-title'); expect(spyOnDialogOpen).toHaveBeenCalled(); }); it('should return the updated node', (done) => { spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(of({id : 'fake-node-updated'})); spyOn(service, 'openAddPermissionDialog').and.returnValue(of({})); - spyOn(contentService, 'getNode').and.returnValue(of(fakePermissionNode)); + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({ node: fakePermissionNode, roles: [] })); service.updateNodePermissionByDialog('fake-node-id', 'fake-title').subscribe((node) => { expect(node.id).toBe('fake-node-updated'); done(); @@ -84,7 +82,7 @@ describe('NodePermissionDialogService', () => { it('should throw an error if the update of the node fails', (done) => { spyOn(nodePermissionService, 'updateNodePermissions').and.returnValue(throwError({error : 'error'})); spyOn(service, 'openAddPermissionDialog').and.returnValue(of({})); - spyOn(contentService, 'getNode').and.returnValue(of(fakePermissionNode)); + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({ node: fakePermissionNode, roles: [] })); service.updateNodePermissionByDialog('fake-node-id', 'fake-title').subscribe(() => { throwError('This call should fail'); }, (error) => { @@ -103,12 +101,12 @@ describe('NodePermissionDialogService', () => { }); it('should not be able to open the dialog showing node permissions', () => { - service.openAddPermissionDialog(fakeForbiddenNode, 'fake-title'); + service.openAddPermissionDialog(fakeForbiddenNode, [], 'fake-title'); expect(spyOnDialogOpen).not.toHaveBeenCalled(); }); it('should return the updated node', (done) => { - spyOn(contentService, 'getNode').and.returnValue(of(fakeForbiddenNode)); + spyOn(nodePermissionService, 'getNodeWithRoles').and.returnValue(of({ node: fakeForbiddenNode, roles: [] })); service.updateNodePermissionByDialog('fake-node-id', 'fake-title').subscribe(() => { throwError('This call should fail'); }, diff --git a/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.ts b/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.ts index d90702ab17..b1a26b7b8f 100644 --- a/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.ts +++ b/lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.ts @@ -15,15 +15,16 @@ * limitations under the License. */ +import { AllowableOperationsEnum, ContentService } from '@alfresco/adf-core'; +import { Node, PermissionElement } from '@alfresco/js-api'; import { MatDialog } from '@angular/material/dialog'; import { Injectable } from '@angular/core'; -import { Subject, Observable, throwError } from 'rxjs'; +import { Observable, Subject, throwError } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { NodePermissionService } from './node-permission.service'; import { AddPermissionDialogComponent } from '../components/add-permission/add-permission-dialog.component'; import { AddPermissionDialogData } from '../components/add-permission/add-permission-dialog-data.interface'; -import { NodeEntry, Node } from '@alfresco/js-api'; -import { NodePermissionService } from './node-permission.service'; -import { ContentService, AllowableOperationsEnum } from '@alfresco/adf-core'; -import { switchMap } from 'rxjs/operators'; +import { RoleModel } from '../models/role.model'; @Injectable({ providedIn: 'root' @@ -37,25 +38,27 @@ export class NodePermissionDialogService { /** * Opens a dialog to add permissions to a node. - * @param node ID of the target node + * @param node target node + * @param roles settable roles for the node * @param title Dialog title * @returns Node with updated permissions */ - openAddPermissionDialog(node: Node, title?: string): Observable { + openAddPermissionDialog(node: Node, roles: RoleModel[], title?: string): Observable { if (this.contentService.hasAllowableOperations(node, AllowableOperationsEnum.UPDATEPERMISSIONS)) { - const confirm = new Subject(); + const confirm = new Subject(); confirm.subscribe({ complete: this.close.bind(this) }); const data: AddPermissionDialogData = { - nodeId: node.id, + node: node, title: title, - confirm: confirm + confirm: confirm, + roles }; - this.openDialog(data, 'adf-add-permission-dialog', '630px'); + this.openDialog(data, 'adf-add-permission-dialog', '800px'); return confirm; } else { const errors = new Error(JSON.stringify({ error: { statusCode: 403 } })); @@ -65,7 +68,7 @@ export class NodePermissionDialogService { } private openDialog(data: any, currentPanelClass: string, chosenWidth: string) { - this.dialog.open(AddPermissionDialogComponent, { data, panelClass: currentPanelClass, width: chosenWidth }); + this.dialog.open(AddPermissionDialogComponent, { data, panelClass: currentPanelClass, width: chosenWidth, restoreFocus: true }); } /** @@ -82,10 +85,10 @@ export class NodePermissionDialogService { * @returns Node with updated permissions */ updateNodePermissionByDialog(nodeId?: string, title?: string): Observable { - return this.contentService.getNode(nodeId, { include: ['allowableOperations'] }) + return this.nodePermissionService.getNodeWithRoles(nodeId) .pipe( - switchMap((node) => { - return this.openAddPermissionDialog(node.entry, title) + switchMap(({node, roles}) => { + return this.openAddPermissionDialog(node, roles, title) .pipe( switchMap((selection) => { return this.nodePermissionService.updateNodePermissions(nodeId, selection); diff --git a/lib/content-services/src/lib/permission-manager/services/node-permission.service.spec.ts b/lib/content-services/src/lib/permission-manager/services/node-permission.service.spec.ts index 59aa288bc4..bed5d0fa12 100644 --- a/lib/content-services/src/lib/permission-manager/services/node-permission.service.spec.ts +++ b/lib/content-services/src/lib/permission-manager/services/node-permission.service.spec.ts @@ -31,6 +31,23 @@ describe('NodePermissionService', () => { let service: NodePermissionService; let nodeService: NodesApiService; let searchApiService: SearchService; + const fakePermissionElements: PermissionElement[] = [ + { + authorityId: fakeAuthorityResults[0].entry.properties['cm:userName'], + name: 'Consumer', + accessStatus: 'ALLOWED' + }, + { + authorityId: fakeAuthorityResults[1].entry.properties['cm:userName'], + name: 'Consumer', + accessStatus: 'ALLOWED' + }, + { + authorityId: fakeAuthorityResults[2].entry.properties['cm:authorityName'], + name: 'Consumer', + accessStatus: 'ALLOWED' + } + ]; setupTestBed({ imports: [ @@ -87,7 +104,7 @@ describe('NodePermissionService', () => { spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); - service.updatePermissionRole(fakeNodeWithOnlyLocally, fakePermission).subscribe((node: Node) => { + service.updatePermissionRole(JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)), fakePermission).subscribe((node: Node) => { expect(node).not.toBeNull(); expect(node.id).toBe('fake-updated-node'); expect(node.permissions.locallySet.length).toBe(1); @@ -119,10 +136,8 @@ describe('NodePermissionService', () => { const fakeNodeCopy = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeCopy)); spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); - spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeSiteNodeResponse)); - spyOn(service, 'getGroupMemberByGroupName').and.returnValue(of(fakeSiteRoles)); - service.updateNodePermissions('fake-node-id', fakeAuthorityResults).subscribe((node: Node) => { + service.updateNodePermissions('fake-node-id', fakePermissionElements).subscribe((node: Node) => { expect(node).not.toBeNull(); expect(node.id).toBe('fake-updated-node'); expect(node.permissions.locallySet.length).toBe(4); @@ -136,7 +151,7 @@ describe('NodePermissionService', () => { const fakeNodeCopy = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); - service.updateLocallySetPermissions(fakeNodeCopy, fakeAuthorityResults, fakeSiteRoles).subscribe((node: Node) => { + service.updateLocallySetPermissions(fakeNodeCopy, fakePermissionElements).subscribe((node: Node) => { expect(node).not.toBeNull(); expect(node.id).toBe('fake-updated-node'); expect(node.permissions.locallySet.length).toBe(4); @@ -150,8 +165,7 @@ describe('NodePermissionService', () => { const fakeNodeCopy = JSON.parse(JSON.stringify(fakeNodeWithoutPermissions)); fakeNodeCopy.permissions.locallySet = undefined; spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); - - service.updateLocallySetPermissions(fakeNodeCopy, fakeAuthorityResults, fakeSiteRoles).subscribe((node: Node) => { + service.updateLocallySetPermissions(fakeNodeCopy, fakePermissionElements).subscribe((node: Node) => { expect(node).not.toBeNull(); expect(node.id).toBe('fake-updated-node'); expect(node.permissions.locallySet.length).toBe(3); @@ -164,31 +178,58 @@ describe('NodePermissionService', () => { it('should fail when user select the same authority and role to add', async(() => { const fakeNodeCopy = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); - const fakeDuplicateAuthority: any = [{ - 'entry': { - 'isFolder': false, - 'search': { - 'score': 0.3541112 - }, - 'isFile': false, - 'name': 'GROUP_EVERYONE', - 'location': 'nodes', - 'id': 'GROUP_EVERYONE', - 'nodeType': 'cm:authorityContainer', - 'properties': { - 'cm:authorityName': 'GROUP_EVERYONE' - }, - 'parentId': '030d833e-da8e-4f5c-8ef9-d809638bd04b' - } + const fakeDuplicateAuthority: PermissionElement [] = [{ + authorityId: 'GROUP_EVERYONE', + accessStatus: 'ALLOWED', + name: 'Contributor' }]; - service.updateLocallySetPermissions(fakeNodeCopy, fakeDuplicateAuthority, ['Contributor']) + service.updateLocallySetPermissions(fakeNodeCopy, fakeDuplicateAuthority) .subscribe(() => { - + fail('should throw exception'); }, (errorMessage) => { expect(errorMessage).not.toBeNull(); expect(errorMessage).toBeDefined(); expect(errorMessage).toBe('PERMISSION_MANAGER.ERROR.DUPLICATE-PERMISSION'); }); })); + + it('should be able to remove the locallyset permission', async(() => { + const fakeNodeCopy = JSON.parse(JSON.stringify(fakeNodeWithoutPermissions)); + fakeNodeCopy.permissions.locallySet = [...fakePermissionElements]; + spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); + service.removePermissions(fakeNodeCopy, [fakePermissionElements[2]]).subscribe((node: Node) => { + expect(node).not.toBeNull(); + expect(node.id).toBe('fake-updated-node'); + expect(node.permissions.locallySet.length).toBe(2); + expect(node.permissions.locallySet[0].authorityId).toBe(fakePermissionElements[0].authorityId); + expect(node.permissions.locallySet[1].authorityId).toBe(fakePermissionElements[1].authorityId); + }); + })); + + it('should be able to replace the locally set', async(() => { + const fakeNodeCopy = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); + fakeNodeCopy.permissions.locallySet = []; + spyOn(nodeService, 'updateNode').and.callFake((nodeId, permissionBody) => returnUpdatedNode(nodeId, permissionBody)); + service.updatePermissions(fakeNodeCopy, fakePermissionElements).subscribe((node: Node) => { + expect(node).not.toBeNull(); + expect(node.id).toBe('fake-updated-node'); + expect(node.permissions.locallySet.length).toBe(3); + expect(node.permissions.locallySet[0].authorityId).toBe(fakePermissionElements[0].authorityId); + expect(node.permissions.locallySet[1].authorityId).toBe(fakePermissionElements[1].authorityId); + expect(node.permissions.locallySet[2].authorityId).toBe(fakePermissionElements[2].authorityId); + }); + })); + + it('should be able to get node and it\'s roles', async(() => { + const fakeNodeCopy = JSON.parse(JSON.stringify(fakeNodeWithOnlyLocally)); + spyOn(nodeService, 'getNode').and.returnValue(of(fakeNodeCopy)); + spyOn(searchApiService, 'searchByQueryBody').and.returnValue(of(fakeSiteNodeResponse)); + spyOn(service, 'getGroupMemberByGroupName').and.returnValue(of(fakeSiteRoles)); + service.getNodeWithRoles('node-id').subscribe(({ node, roles }) => { + expect(node).toBe(fakeNodeCopy); + expect(roles.length).toBe(4); + expect(roles[0].role).toBe('SiteCollaborator'); + }); + })); }); diff --git a/lib/content-services/src/lib/permission-manager/services/node-permission.service.ts b/lib/content-services/src/lib/permission-manager/services/node-permission.service.ts index 00aa037030..12674f716a 100644 --- a/lib/content-services/src/lib/permission-manager/services/node-permission.service.ts +++ b/lib/content-services/src/lib/permission-manager/services/node-permission.service.ts @@ -15,12 +15,13 @@ * limitations under the License. */ +import { AlfrescoApiService, NodesApiService, SearchService, TranslationService } from '@alfresco/adf-core'; +import { Group, GroupMemberEntry, GroupMemberPaging, Node, PathElement, PermissionElement, Person, QueryBody } from '@alfresco/js-api'; import { Injectable } from '@angular/core'; -import { Observable, of, from, throwError } from 'rxjs'; -import { AlfrescoApiService, SearchService, NodesApiService, TranslationService } from '@alfresco/adf-core'; -import { QueryBody, Node, NodeEntry, PathElement, GroupMemberEntry, GroupMemberPaging, PermissionElement } from '@alfresco/js-api'; -import { switchMap, map } from 'rxjs/operators'; +import { forkJoin, from, Observable, of, throwError } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { PermissionDisplayModel } from '../models/permission.model'; +import { RoleModel } from '../models/role.model'; @Injectable({ providedIn: 'root' @@ -96,27 +97,21 @@ export class NodePermissionService { * @param permissionList New permission settings * @returns Node with updated permissions */ - updateNodePermissions(nodeId: string, permissionList: NodeEntry[]): Observable { + updateNodePermissions(nodeId: string, permissionList: PermissionElement[]): 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)) + switchMap((node) => this.updateLocallySetPermissions(node, permissionList)) ); } /** * Updates the locally set permissions for a node. * @param node ID of the target node - * @param nodes Permission settings - * @param nodeRole Permission role + * @param permissions Permission settings * @returns Node with updated permissions */ - updateLocallySetPermissions(node: Node, nodes: NodeEntry[], nodeRole: string[]): Observable { + updateLocallySetPermissions(node: Node, permissions: PermissionElement[]): Observable { const permissionBody = { permissions: { locallySet: []} }; - const permissionList = this.transformNodeToPermissionElement(nodes, nodeRole[0]); + const permissionList = permissions; const duplicatedPermissions = this.getDuplicatedPermissions(node.permissions.locallySet, permissionList); if (duplicatedPermissions.length > 0) { const list = duplicatedPermissions.map((permission) => 'authority -> ' + permission.authorityId + ' / role -> ' + permission.name).join(', '); @@ -146,18 +141,6 @@ export class NodePermissionService { oldPermission.name === newPermission.name; } - private transformNodeToPermissionElement(nodes: NodeEntry[], nodeRole: any): PermissionElement[] { - return nodes.map((node) => { - return { - 'authorityId': node.entry.properties['cm:authorityName'] ? - node.entry.properties['cm:authorityName'] : - node.entry.properties['cm:userName'], - 'name': nodeRole, - 'accessStatus': 'ALLOWED' - }; - }); - } - /** * Removes a permission setting from a node. * @param node ID of the target node @@ -227,4 +210,97 @@ export class NodePermissionService { }; } + getLocalPermissions(node: Node): PermissionDisplayModel[] { + const result: PermissionDisplayModel[] = []; + + if (node?.permissions?.locallySet) { + node.permissions.locallySet.forEach((permissionElement) => { + result.push(new PermissionDisplayModel(permissionElement)); + }); + } + + return result; + } + + getInheritedPermission(node: Node): PermissionDisplayModel[] { + const result: PermissionDisplayModel[] = []; + + if (node?.permissions?.inherited) { + node.permissions.inherited.forEach((permissionElement) => { + const permissionInherited = new PermissionDisplayModel(permissionElement); + permissionInherited.isInherited = true; + result.push(permissionInherited); + }); + } + return result; + } + + /** + * Removes permissions setting from a node. + * @param node target node with permission + * @param permissions Permissions to remove + * @returns Node with modified permissions + */ + removePermissions(node: Node, permissions: PermissionElement[]): Observable { + const permissionBody = { permissions: { locallySet: [] } }; + + permissions.forEach((permission) => { + const index = node.permissions.locallySet.findIndex((locallySet) => locallySet.authorityId === permission.authorityId); + if (index !== -1) { + node.permissions.locallySet.splice(index, 1); + } + }); + permissionBody.permissions.locallySet = node.permissions.locallySet; + return this.nodeService.updateNode(node.id, permissionBody); + } + + /** + * updates permissions setting from a node. + * @param node target node with permission + * @param permissions Permissions to update + * @returns Node with modified permissions + */ + updatePermissions(node: Node, permissions: PermissionElement[]): Observable { + const permissionBody = { permissions: { locallySet: [] } }; + permissionBody.permissions.locallySet = permissions; + return this.nodeService.updateNode(node.id, permissionBody); + } + + /** + * Gets all node detail for nodeId along with settable permissions. + * @param nodeId Id of the node + * @returns node and it's associated roles { node: Node; roles: RoleModel[] } + */ + getNodeWithRoles(nodeId: string): Observable<{ node: Node; roles: RoleModel[] }> { + return this.nodeService.getNode(nodeId).pipe( + switchMap(node => { + return forkJoin({ + node: of(node), + roles: this.getNodeRoles(node) + .pipe( + map(_roles => _roles.map(role => ({ role, label: role })) + ) + ) + }); + }) + ); + } + + transformNodeToUserPerson(node: Node): { person: Person, group: Group } { + let person = null, group = null; + if (node.nodeType === 'cm:person') { + const firstName = node.properties['cm:firstName']; + const lastName = node.properties['cm:lastName']; + const email = node.properties['cm:email']; + const id = node.properties['cm:userName']; + person = new Person({ id, firstName, lastName, email}); + } + + if (node.nodeType === 'cm:authorityContainer') { + const displayName = node.properties['cm:authorityDisplayName'] || node.properties['cm:authorityName']; + const id = node.properties['cm:authorityName']; + group = new Group({ displayName, id }); + } + return { person, group }; + } } diff --git a/lib/content-services/src/lib/styles/_index.scss b/lib/content-services/src/lib/styles/_index.scss index 27d0fe336c..366a968974 100644 --- a/lib/content-services/src/lib/styles/_index.scss +++ b/lib/content-services/src/lib/styles/_index.scss @@ -28,7 +28,9 @@ @import '../version-manager/version-comparison.component'; @import '../content-type/content-type-dialog.component'; @import '../aspect-list/aspect-list.component'; -@import '../aspect-list//aspect-list-dialog.component'; +@import '../aspect-list/aspect-list-dialog.component'; +@import '../permission-manager/components/permission-container/permission-container.component'; +@import '../permission-manager/components/user-icon-column/user-icon-column.component'; @mixin adf-content-services-theme($theme) { @include adf-breadcrumb-theme($theme); @@ -48,6 +50,8 @@ @include adf-content-node-selector-dialog-theme($theme); @include adf-content-metadata-module-theme($theme); @include adf-permission-list-theme($theme); + @include adf-permission-container-theme($theme); + @include adf-user-icon-column-theme($theme); @include adf-add-permission-theme($theme); @include adf-add-permission-dialog-theme($theme); @include adf-add-permission-panel-theme($theme); diff --git a/lib/core/data-column/data-column-header.component.ts b/lib/core/data-column/data-column-header.component.ts new file mode 100644 index 0000000000..3796e14f33 --- /dev/null +++ b/lib/core/data-column/data-column-header.component.ts @@ -0,0 +1,39 @@ +/*! + * @license + * Copyright 2019 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. + */ + +/* tslint:disable:component-selector no-input-rename */ + +import { Component, ContentChild, TemplateRef } from '@angular/core'; +import { DataColumnComponent } from './data-column.component'; + +@Component({ + selector: 'adf-data-column-header', + template: '' +}) +export class DateColumnHeaderComponent { + + @ContentChild(TemplateRef) + public template: TemplateRef; + + constructor(private columnComponent: DataColumnComponent) {} + + ngAfterContentInit() { + if (this.columnComponent) { + this.columnComponent.header = this.template; + } + } +} diff --git a/lib/core/data-column/data-column.component.ts b/lib/core/data-column/data-column.component.ts index 7bd0ce480e..2feeda74a7 100644 --- a/lib/core/data-column/data-column.component.ts +++ b/lib/core/data-column/data-column.component.ts @@ -82,6 +82,9 @@ export class DataColumnComponent implements OnInit { @Input() sortingKey: string; + /** Data column header template */ + header?: TemplateRef; + ngOnInit() { if (!this.srTitle && this.key === '$thumbnail') { this.srTitle = 'Thumbnail'; diff --git a/lib/core/data-column/data-column.module.ts b/lib/core/data-column/data-column.module.ts index b1b73e0a13..542648b4c3 100644 --- a/lib/core/data-column/data-column.module.ts +++ b/lib/core/data-column/data-column.module.ts @@ -20,6 +20,7 @@ import { NgModule } from '@angular/core'; import { DataColumnListComponent } from './data-column-list.component'; import { DataColumnComponent } from './data-column.component'; +import { DateColumnHeaderComponent } from './data-column-header.component'; @NgModule({ imports: [ @@ -27,11 +28,13 @@ import { DataColumnComponent } from './data-column.component'; ], declarations: [ DataColumnComponent, - DataColumnListComponent + DataColumnListComponent, + DateColumnHeaderComponent ], exports: [ DataColumnComponent, - DataColumnListComponent + DataColumnListComponent, + DateColumnHeaderComponent ] }) export class DataColumnModule {} diff --git a/lib/core/data-column/public-api.ts b/lib/core/data-column/public-api.ts index cf7cd88bfe..87205ff9f9 100644 --- a/lib/core/data-column/public-api.ts +++ b/lib/core/data-column/public-api.ts @@ -17,5 +17,6 @@ export * from './data-column-list.component'; export * from './data-column.component'; +export * from './data-column-header.component'; export * from './data-column.module'; diff --git a/lib/core/datatable/components/datatable/datatable.component.html b/lib/core/datatable/components/datatable/datatable.component.html index 94141161b7..e39cf4e49d 100644 --- a/lib/core/datatable/components/datatable/datatable.component.html +++ b/lib/core/datatable/components/datatable/datatable.component.html @@ -33,9 +33,12 @@ [attr.tabindex]="isHeaderVisible() ? 0 : null" [attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null" adf-drop-zone dropTarget="header" [dropColumn]="col"> - {{ col.title | translate}} - {{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }} - + + {{ col.title | translate}} + {{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }} + + +
diff --git a/lib/core/datatable/data/data-column.model.ts b/lib/core/datatable/data/data-column.model.ts index 21f0f14c4e..fd9cbb0d98 100644 --- a/lib/core/datatable/data/data-column.model.ts +++ b/lib/core/datatable/data/data-column.model.ts @@ -43,4 +43,5 @@ export interface DataColumn { editable?: boolean; focus?: boolean; sortingKey?: string; + header?: TemplateRef; } diff --git a/lib/core/datatable/data/object-datacolumn.model.ts b/lib/core/datatable/data/object-datacolumn.model.ts index d89f5a65f5..82c1c21224 100644 --- a/lib/core/datatable/data/object-datacolumn.model.ts +++ b/lib/core/datatable/data/object-datacolumn.model.ts @@ -32,6 +32,7 @@ export class ObjectDataColumn implements DataColumn { copyContent?: boolean; focus?: boolean; sortingKey?: string; + header?: TemplateRef; constructor(input: any) { this.key = input.key; @@ -45,5 +46,6 @@ export class ObjectDataColumn implements DataColumn { this.copyContent = input.copyContent; this.focus = input.focus; this.sortingKey = input.sortingKey; + this.header = input.template; } } diff --git a/lib/testing/src/lib/content-services/dialog/add-permissions-dialog.page.ts b/lib/testing/src/lib/content-services/dialog/add-permissions-dialog.page.ts index bbcc79a3a5..a7bf3d18f5 100644 --- a/lib/testing/src/lib/content-services/dialog/add-permissions-dialog.page.ts +++ b/lib/testing/src/lib/content-services/dialog/add-permissions-dialog.page.ts @@ -28,15 +28,12 @@ const column = { export class AddPermissionsDialogPage { dataTableComponentPage: DataTableComponentPage = new DataTableComponentPage(); + userRoleDataTableComponentPage: DataTableComponentPage = new DataTableComponentPage(element(by.css('[data-automation-id="adf-user-role-selection-table"]'))); addPermissionDialog = element(by.css('adf-add-permission-dialog')); searchUserInput = element(by.id('searchInput')); searchResults = element(by.css('#adf-add-permission-authority-results #adf-search-results-content')); - addButton = element(by.id('add-permission-dialog-confirm-button')); - permissionInheritedButton = element.all(by.css('div[class="app-inherit_permission_button"] button')).first(); - noPermissions = element(by.id('adf-no-permissions-template')); - deletePermissionButton = element(by.css('button[data-automation-id="adf-delete-permission-button"]')); - permissionDisplayContainer = element(by.id('adf-permission-display-container')); + addButton = element(by.css('[data-automation-id="add-permission-dialog-confirm-button"]')); closeButton = element(by.id('add-permission-dialog-close-button')); async clickCloseButton(): Promise { @@ -70,52 +67,17 @@ export class AddPermissionsDialogPage { await BrowserActions.click(this.addButton); } - async checkUserIsAdded(name: string): Promise { - const userOrGroupName = element(by.css('div[data-automation-id="text_' + name + '"]')); - await BrowserVisibility.waitUntilElementIsVisible(userOrGroupName); - } - - async checkGroupIsAdded(name: string): Promise { - const userOrGroupName = element(by.css('div[data-automation-id="text_GROUP_' + name + '"]')); - await BrowserVisibility.waitUntilElementIsVisible(userOrGroupName); - } - - async checkUserIsDeleted(name: string): Promise { - const userOrGroupName = element(by.css('div[data-automation-id="text_' + name + '"]')); - await BrowserVisibility.waitUntilElementIsNotVisible(userOrGroupName); - } - - async checkPermissionInheritedButtonIsDisplayed() { - await BrowserVisibility.waitUntilElementIsVisible(this.permissionInheritedButton); - } - - async clickPermissionInheritedButton(): Promise { - await BrowserActions.click(this.permissionInheritedButton); - } - - async clickDeletePermissionButton(): Promise { - await BrowserActions.click(this.deletePermissionButton); - } - - async checkNoPermissionsIsDisplayed(): Promise { - await BrowserVisibility.waitUntilElementIsVisible(this.noPermissions); - } - - async getPermissionInheritedButtonText(text: string): Promise { - await BrowserVisibility.waitUntilElementHasText(this.permissionInheritedButton, text); - } - async checkPermissionsDatatableIsDisplayed(): Promise { await BrowserVisibility.waitUntilElementIsVisible(element(by.css('[class*="adf-datatable-permission"]'))); } async getRoleCellValue(rowName: string): Promise { - const locator = this.dataTableComponentPage.getCellByRowContentAndColumn('Authority ID', rowName, column.role); + const locator = this.dataTableComponentPage.getCellByRowContentAndColumn('Users and Groups', rowName, column.role); return BrowserActions.getText(locator); } async clickRoleDropdownByUserOrGroupName(name: string): Promise { - const row = this.dataTableComponentPage.getRow('Authority ID', name); + const row = this.dataTableComponentPage.getRow('Users and Groups', name); await BrowserActions.click(row.element(by.id('adf-select-role-permission'))); } @@ -127,12 +89,23 @@ export class AddPermissionsDialogPage { await new DropdownPage().selectOption(name); } - async checkPermissionContainerIsDisplayed(): Promise { - await BrowserVisibility.waitUntilElementIsVisible(this.permissionDisplayContainer); - } - async checkUserOrGroupIsDisplayed(name: string): Promise { const userOrGroupName = element(by.cssContainingText('mat-list-option .mat-list-text', name)); await BrowserVisibility.waitUntilElementIsVisible(userOrGroupName); } + + async addButtonIsEnabled(): Promise { + return this.addButton.isEnabled(); + } + + async clickAddButton(): Promise { + await BrowserActions.click(this.addButton); + } + + async selectRole(name: string, role) { + const row = this.userRoleDataTableComponentPage.getRow('Users and Groups', name); + await BrowserActions.click(row.element(by.css('[id="adf-select-role-permission"] .mat-select-trigger'))); + await this.getRoleDropdownOptions(); + await this.selectOption(role); + } } diff --git a/lib/testing/src/lib/core/models/user.model.ts b/lib/testing/src/lib/core/models/user.model.ts index 3d4d8c5320..cdb64f14ba 100644 --- a/lib/testing/src/lib/core/models/user.model.ts +++ b/lib/testing/src/lib/core/models/user.model.ts @@ -49,6 +49,10 @@ export class UserModel { this.id = details.id ? details.id : this.id; } + get fullName() { + return `${this.firstName ?? ''} ${this.lastName ?? ''}`; + } + getAPSModel() { return new UserRepresentation({ firstName: this.firstName, diff --git a/lib/testing/src/lib/core/pages/data-table-component.page.ts b/lib/testing/src/lib/core/pages/data-table-component.page.ts index 11f4fff6ee..75ea673918 100644 --- a/lib/testing/src/lib/core/pages/data-table-component.page.ts +++ b/lib/testing/src/lib/core/pages/data-table-component.page.ts @@ -345,7 +345,7 @@ export class DataTableComponentPage { } getRow(columnName: string, columnValue: string): ElementFinder { - return this.rootElement.all(by.xpath(`//div[@title='${columnName}']//div[contains(@data-automation-id, '${columnValue}')]//ancestor::adf-datatable-row[contains(@class, 'adf-datatable-row')]`)).first(); + return this.rootElement.all(by.xpath(`//div[starts-with(@title, '${columnName}')]//div[contains(@data-automation-id, '${columnValue}')]//ancestor::adf-datatable-row[contains(@class, 'adf-datatable-row')]`)).first(); } getRowByIndex(index: number): ElementFinder {