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 1cfe99c66f..4e13e1b705 100644 Binary files a/docs/docassets/images/adf-permission-list.png and b/docs/docassets/images/adf-permission-list.png differ 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 {